Skip to main content

Run a Validator Node

Everything on this page happens before the Orbinum team hears from you. By the end your node is synced and your session keys are registered on-chain — which is exactly the state addValidator requires.

Prerequisites

A server meeting the validator requirements, Docker installed, and a GHCR token — see Installation.


1. Authenticate with GHCR

The node image is private. Log in once per host with a personal access token scoped to read:packages:

echo <GITHUB_TOKEN> | docker login ghcr.io -u <github_user> --password-stdin

Piping the token keeps it out of your shell history. Docker stores the credential in ~/.docker/config.json, so this survives reboots and automatic image updates.


2. Clone node-deploy and configure

The Compose file and the chain spec ship together:

git clone https://github.com/orbinum/node-deploy.git
cd node-deploy/testnet/validator
cp .env.example .env

Every variable is documented in the file. These are the ones that matter:

VariableValueWhy
VALIDATOR_NAMEanything identifiableShown on telemetry
VALIDATOR_NODE_KEYopenssl rand -hex 32Your stable libp2p identity
TELEMETRY_URLleave the defaultLevel 1 publishes your validator address to the dashboard
METRICS_BINDyour private IP, or leave 127.0.0.1The default fails closed and exposes nothing
PUBLIC_ADDRleave emptyThe node auto-detects its public address
RESERVED_NODESleave emptySee the warning below
Leave RESERVED_NODES empty

RESERVED_NODES exists for Orbinum's own validators, which sit on a private subnet and pin each other with --reserved-only. That flag restricts the node to peers on its reserved list — including refusing the bootnodes.

Setting it on a public validator with an incomplete list isolates the node: zero peers, stuck at block #0, no error in the logs. An independent operator leaves it empty and discovers the network normally.

You do not need bootnodes

testnet-spec.json already carries them, and the Compose file already mounts it. There is nothing to fetch, paste, or add yourself to.


3. Open the firewall

sudo ufw allow 30333/tcp   # P2P — must be reachable from the internet
sudo ufw allow 22/tcp # SSH
sudo ufw enable

No rule for 9944, and none for 9615 unless your monitoring host is on a private network that needs it.


4. Start the node

docker compose pull
docker compose up -d
docker compose logs -f orbinum-validator

The Compose file already passes --validator, --no-mdns, the chain spec and the telemetry flags — there is nothing to add on the command line.

Watch the logs for the node's own peer identity at startup, then a stream of import messages while it catches up. Wait until it reports idling at the chain head before continuing; inserting keys into a syncing node works, but you cannot tell a healthy node from a stalled one until it has caught up.

Editing .env needs a recreate, not a restart

A container's command line is fixed when it is created, so docker compose restart brings the node back with its old arguments and an edited .env appears to do nothing:

docker compose up -d --force-recreate orbinum-validator

To see what a running node actually received:

docker inspect orbinum-validator --format '{{join .Config.Cmd " "}}'

5. Confirm it is synced

docker exec orbinum-validator curl -s -H 'Content-Type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"system_health"}' \
http://localhost:9944
{ "jsonrpc": "2.0", "result": { "peers": 4, "isSyncing": false, "shouldHavePeers": true }, "id": 1 }

You want isSyncing: false and peers of at least 2. peers: 0 means the node cannot reach the bootnodes — check that port 30333 is genuinely reachable from outside, and that RESERVED_NODES is empty.

Note the docker exec: port 9944 is not published to the host, so this command runs inside the container. That is deliberate — see requirements.


6. Generate session keys

docker exec orbinum-validator curl -s -H 'Content-Type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"author_rotateKeys"}' \
http://localhost:9944

The result is a 0x string of 128 hex characters — 64 bytes, being your Aura sr25519 public key followed by your GRANDPA ed25519 public key, in the order the runtime declares them.

The private halves were written into the node's keystore at /data/chains/orbinum_testnet/keystore and never left the server. Only the public blob above goes on-chain.

Run this once

Calling author_rotateKeys again generates a new pair and the blob you saved becomes stale. If you rotate after registering, you must submit session.setKeys again with the new value or your node stops authoring at the next session.

Copy the blob. You need it in the next step.


7. Submit session.setKeys

This is an on-chain extrinsic signed by your validator account — the account you will send to the Orbinum team.

  1. Open Polkadot.js Apps and connect it to wss://rpc-1.testnet.orbinum.io.
  2. Go to Developer → Extrinsics.
  3. Select your validator account, then sessionsetKeys(keys, proof).
  4. Paste the blob from step 6 into keys.
  5. Put 0x into proof.
  6. Submit and sign.

proof is unused by this runtime, so the empty value is correct — it exists for chains that require a possession proof over the session keys.

Verify it landed. In Developer → Chain state, query session.nextKeys(yourAccount). It must return the same blob you submitted. If it returns nothing, the extrinsic did not finalize and the next step will fail.

This is the gate

addValidator is rejected with NoSessionKeys unless this extrinsic has already finalized. Do not email the team before session.nextKeys returns your blob.


Next Steps