Joining the testnet using a custom genesis
A fresh Demos node ships with a defaultdata/genesis.json that boots an isolated single-validator chain. To join an existing network — testnet or a privately restored chain — you need:
- 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.
- At least one bootstrap peer to discover the rest of the network from.
./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.
What’s in genesis.json
data/genesis.json defines:
- The chain’s bootstrap accounts (GCR balances at height 0).
- The initial validator set.
- The
forkssection — activation heights for hard forks (e.g.osDenomination). These are loaded intoSharedState.forkConfigat node startup and gate consensus-relevant code paths.
forks section, because a different activation height produces a different chain.
A genesis snippet with a scheduled fork looks like:
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:
| Field | Description |
|---|---|
properties | Chain identity: id, name, currency. |
mutables | Tunable genesis values such as minBlocksForValidationOnlineStatus. |
forks | Per-fork activation heights (see Network Forks). Every peer must agree on this section. |
balances | Bootstrap account balances as [pubkey, balance] tuples. The balance side may be a decimal string, integer, or bigint-string and is coerced to a bigint in OS denomination; malformed or negative values cause the node to fail loudly at load. |
timestamp | Genesis block timestamp (unix seconds, as a string). |
status | Genesis block status (e.g. confirmed). |
validators | Initial validator set. Each entry: address, status, connection_url (or null), staked_amount (raw bigint string), first_seen, valid_at. Validator state codes are documented in Validator Lifecycle. |
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:
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:
/info) so you can put it in your peerlist:
Track 1: Docker Compose (recommended)
Track 1 stores chain runtime data in the named volumedemos_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.jsonin 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
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
Createdocker-compose.override.yml next to docker-compose.yml:
Seed the bootstrap peerlist
A fresh node has an emptydemos_peerlist.json and cannot discover peers on its own. Seed the volume before first start:
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.
demos_peerlist.json:
-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 loadeddata/genesis.json and, before syncing the first block from a peer, compares it against the peer’s reported genesis hash. If they differ:
- The peer is on a different network (e.g. you pointed at mainnet with a testnet genesis).
- You edited
data/genesis.jsonafter first boot without also wiping chain data (docker compose down -von Track 1,./run -c trueon Track 2). - The peer has rotated genesis (re-deploy after a custom-genesis restore) and you have not pulled the new one.
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.