Skip to main content

Joining the testnet using a custom genesis

A fresh Demos node ships with a default data/genesis.json that boots an isolated single-validator chain. To join an existing network — testnet or a privately restored chain — you need:
  1. The same genesis file every other peer is using. The genesis hash is checked at startup, and a mismatch is unrecoverable: consensus can never form, and the node will exit.
  2. At least one bootstrap peer to discover the rest of the network from.
Both Track 1 (Docker Compose) and Track 2 (bare metal ./run) consume the same data/genesis.json file — they only differ in where the file needs to live for the node process to read it.
Do not invent bootstrap hostnames or genesis URLs. Get the canonical data/genesis.json and a bootstrap peerlist from the Demos team or from a known healthy peer. Pointing at the wrong genesis simply means the node refuses to sync.

What’s in genesis.json

data/genesis.json defines:
  • The chain’s bootstrap accounts (GCR balances at height 0).
  • The initial validator set.
  • The forks section — activation heights for hard forks (e.g. osDenomination). These are loaded into SharedState.forkConfig at node startup and gate consensus-relevant code paths.
For the fork system in detail see Network Forks. The short version: every node on a network must agree on the forks section, because a different activation height produces a different chain. A genesis snippet with a scheduled fork looks like:
A genesis with no forks section, or with activationHeight: null, leaves every fork unscheduled — the node behaves bit-identically to a pre-fork node.

Full genesis schema

data/genesis.json has the following top-level structure:
The balances array is applied on top of any restored snapshot rows (see mergeGenesisBalances), so a fresh single-validator chain typically ships with an empty balances: []. Account rows themselves follow the gcr_main structure.

Get the genesis from a healthy peer

Every running node exposes its current genesis at /genesis. To pull one from a known peer:
Replace NODE_URL with the public URL of a peer the team has told you to bootstrap from. Verify the file is valid JSON before continuing:
You will also want that peer’s public key (from /info) so you can put it in your peerlist:
Track 1 stores chain runtime data in the named volume demos_node_data. The bundled data/ directory (including the default genesis.json) is baked into the node image at build time — to override it, you can either:
  • (A) Edit data/genesis.json in the repo and rebuild the image, or
  • (B) Mount your custom genesis over the in-container path via a compose override.

Option A: edit and rebuild

The down -v step is required: starting against the old demos_pgdata with a different genesis hash will refuse to sync. Your .demos_identity lives in demos_node_state and will also be deleted by down -v — back it up first if you need to keep it (see Backing up and restoring a node).

Option B: bind-mount the genesis

Create docker-compose.override.yml next to docker-compose.yml:
Then:
The bind mount is read-only and authoritative — the node reads it at startup just like the baked-in copy.

Seed the bootstrap peerlist

A fresh node has an empty demos_peerlist.json and cannot discover peers on its own. Seed the volume before first start:
Replace 0xPEERPUBLICKEY and NODE_URL with the values from the peer’s /info and the URL the team gave you.

Track 2: Bare metal ./run (advanced)

Track 2 reads data/genesis.json directly from the repo path — no volumes involved.
Example demos_peerlist.json:
Then start the node with -c true to wipe the local Postgres (the new genesis is incompatible with the old chain data):

Genesis hash check

On startup the node computes the hash of its loaded data/genesis.json and, before syncing the first block from a peer, compares it against the peer’s reported genesis hash. If they differ:
The node terminates. Causes are almost always:
  • The peer is on a different network (e.g. you pointed at mainnet with a testnet genesis).
  • You edited data/genesis.json after first boot without also wiping chain data (docker compose down -v on Track 1, ./run -c true on Track 2).
  • The peer has rotated genesis (re-deploy after a custom-genesis restore) and you have not pulled the new one.
Re-pull the genesis from the peer and start over.
Genesis hash is height-0 only. Once both sides agree there, ongoing consensus is governed by the activated forks (see Network Forks) and the live validator set.