This project provides a simple server to track the upgrade status of nodes in a network. It exposes a web-based dashboard to visualize the upgrade progress and an API for nodes to submit their status.
- Node.js (v18 or newer)
- npm
- A PostgreSQL database. You can easily set one up for free on Neon.
-
Clone the repository:
git clone https://github.com/o1-labs/hardfork-upgrade-tracking cd hardfork-upgrade-tracking -
Install dependencies:
npm install
-
Set up environment variables:
Create a
.envfile by copying the example file:cp .env.example .env
Edit the
.envfile and set theDATABASE_URLto your PostgreSQL connection string. For example:DATABASE_URL="postgresql://user:password@localhost:5432/hardfork_tracking?schema=public"You can also optionally change the
RELEASE_PERCENTAGE.Optional —
SHOW_NON_BP_NODES. By default the dashboard table shows one row per block producer, and nodes reporting no block producer key are hidden (they are still stored, and readable viaGET /submit/stats). SetSHOW_NON_BP_NODES=trueto admit those nodes as their own rows, keyed bypeer_id. This is meant for networks where we operate no block producers of our own — on mainnet the fleet is archive + seed nodes, so every node we run reports a null BP key and the default view is permanently empty. With the flag on the count cards read "Nodes" rather than "Block Producers", since the rows are no longer one-per-block-producer. Stake figures are identical either way: keyless rows carry no stake and are excluded from the stake math, so the release-target gate still measures block producer stake only. -
Apply the database schema:
Run the following command to create the tables in your database:
npx prisma db push
Alternatively, you can use migrations:
npx prisma migrate dev
You can run the server in development mode, which will automatically restart on file changes:
npm run devThe server will start on http://localhost:3000.
Build the project first and then start the server:
npm run build
npm run startDocker images are published to GitHub Container Registry at ghcr.io/o1-labs/hardfork-upgrade-tracking.
Pull and run a pre-built image:
docker run --rm -p 3000:3000 \
-e DATABASE_URL="postgresql://user:password@host:5432/db" \
-e RELEASE_PERCENTAGE="65" \
-e SHOW_NON_BP_NODES="false" \
ghcr.io/o1-labs/hardfork-upgrade-tracking:latestBuild locally using the Makefile:
# Using Docker
make build
# Using Podman
make podman-buildRun the locally built image:
# Using Docker
DATABASE_URL="postgresql://user:password@host:5432/db" RELEASE_PERCENTAGE=65 make run
# Using Podman
DATABASE_URL="postgresql://user:password@host:5432/db" RELEASE_PERCENTAGE=65 make podman-runThe container automatically runs prisma db push on startup to apply the database schema.
Demo environment with podman-compose:
A complete demo environment with PostgreSQL and optional Mina node is available in the demo/ directory:
cd demo
# Start tracker with postgres
podman-compose up -d
# Or include a Mina daemon that submits stats
podman-compose --profile with-mina up -dSee demo/README.md for full instructions.
Once the server is running, you can view the dashboard by opening your web browser and navigating to:
The dashboard displays the current upgrade progress of the network, including:
- Percentage of active stake that has upgraded
- Total number of upgraded vs non-upgraded nodes
- Block producer upgrade status with stake percentages
- Export functionality to download data as CSV
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/ |
No | HTML dashboard |
Nodes submit their status to track upgrade progress across the network.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/submit/stats |
No | Submit node stats |
GET |
/submit/stats |
No | Get all node stats |
GET |
/submit/stats/:peerId |
No | Get stats for a specific peer |
Request Body:
{
"max_observed_block_height": 8392,
"commit_hash": "a1b2c3d4",
"chain_id": "mainnet",
"peer_id": "12D3KooWL7tVWT3LpBDv3p5bLNKm2w5V51s1A4Q4Zg4Q4Yq4b4Q4",
"peer_count": 10,
"timestamp": "2026-01-26T10:00:00.000Z",
"block_producer_public_key": "B62q..."
}Example:
curl -X POST http://localhost:3000/submit/stats \
-H "Content-Type: application/json" \
-d '{
"max_observed_block_height": 8392,
"commit_hash": "a1b2c3d4",
"chain_id": "mainnet",
"peer_id": "12D3KooWL7tVWT3LpBDv3p5bLNKm2w5V51s1A4Q4Zg4Q4Yq4b4Q4",
"peer_count": 10,
"timestamp": "2026-01-26T10:00:00.000Z",
"block_producer_public_key": "B62qrPN5Y5yq8kGE3FbVKbGTdTAJNdtNtS5sKqLYxhYGDzuDv2VRvgH"
}'curl http://localhost:3000/submit/statscurl http://localhost:3000/submit/stats/12D3KooWL7tVWT3LpBDv3p5bLNKm2w5V51s1A4Q4Zg4Q4Yq4b4Q4Block producer stake data is used to calculate the percentage of active stake that has upgraded.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/block-producers |
No | List all block producers |
GET |
/block-producers/last-sync |
No | Get last CSV sync timestamp |
GET |
/block-producers/:publicKey |
No | Get a specific block producer |
POST |
/block-producers/upload |
Yes | Upload CSV data |
curl http://localhost:3000/block-producerscurl http://localhost:3000/block-producers/last-synccurl http://localhost:3000/block-producers/B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9The CSV must have the following columns:
bp_public_key- Block producer public key (B62...)total_stake- Total stake amountnum_delegators- Number of delegatorsis_active- Whether BP is active (true/false)percent_total_stake- Percentage of total stake, on a 0–100 scale (e.g.12.5for 12.5%)percent_total_active_stake- Percentage of active stake, on a 0–100 scale (e.g.12.5for 12.5%)
curl -X POST http://localhost:3000/block-producers/upload \
-H "Authorization: Bearer $CSV_UPLOAD_TOKEN" \
-H "Content-Type: text/csv" \
--data-binary @block_producers.csvA node is considered "upgraded" if its commit_hash matches one of the valid commits in the database.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/valid-commits |
No | List all valid commits |
POST |
/valid-commits |
Yes | Add commit(s) |
DELETE |
/valid-commits/:hash |
Yes | Remove a commit |
curl http://localhost:3000/valid-commitscurl -X POST http://localhost:3000/valid-commits \
-H "Authorization: Bearer $CSV_UPLOAD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hash": "f1e40a7ef71c799b5af8821ff85aadb44f53a377", "label": "3.3.0-compatible"}'curl -X POST http://localhost:3000/valid-commits \
-H "Authorization: Bearer $CSV_UPLOAD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"commits": [{"hash": "abc123", "label": "v1.0"}, {"hash": "def456"}]}'curl -X DELETE http://localhost:3000/valid-commits/abc123 \
-H "Authorization: Bearer $CSV_UPLOAD_TOKEN"Protected endpoints require a bearer token. Set the CSV_UPLOAD_TOKEN environment variable:
# In .env
CSV_UPLOAD_TOKEN="your-secret-token"In production, this token should be stored in a secrets manager (e.g., GCP Secret Manager) and injected as an environment variable.
Run the test suite:
npm testTests cover:
- CSV parsing for block producer data
- Stake calculation logic with deduplication
- Upgrade check (commit hash validation)
- Template helper functions
Tests are automatically run in CI on every pull request and must pass before merging.