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.
A server meeting the validator requirements and Docker with the Compose plugin — see Installation. No registry token: the image is public.
1. 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:
| Variable | Value | Why |
|---|---|---|
VALIDATOR_NAME | anything identifiable | Shown on telemetry |
VALIDATOR_NODE_KEY | openssl rand -hex 32 | Your stable libp2p identity |
TELEMETRY_URL | leave the default | Level 1 publishes your validator address to the dashboard |
METRICS_BIND | your private IP, or leave 127.0.0.1 | The default fails closed and exposes nothing |
PUBLIC_ADDR | leave empty | The node auto-detects its public address |
RESERVED_NODES | leave empty | Not additive — a partial list isolates the node. See Peering |
SYNC_MODE | leave empty unless bootstrapping | Full sync is the safe default — see the tip below |
testnet-spec.json already carries them, and the Compose file already mounts it.
There is nothing to fetch, paste, or add yourself to.
SYNC_MODE=--sync warp skips replaying every block, but only on a brand new
volume, and the node keeps no block bodies before the sync point — see
the caveats before setting it.
2. 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.
3. 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. The first lines of
the log are the hardware benchmark, which must stay on.
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 later needs a recreate, not a restart — see
Updates:
docker compose up -d --force-recreate orbinum-validator
4. 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: it runs the request inside the container, where the RPC
port is always 9944. Compose does publish that port, but only on loopback
(RPC_BIND=127.0.0.1) and under whatever RPC_PORT you set — so if you changed
RPC_PORT to, say, 26722, then from the host it is
curl http://localhost:26722/ while inside the container it stays 9944. Using
docker exec sidesteps the difference, which is why every command on this page
is written that way. The port is never reachable from the internet, and must not
be: the binary forces --rpc-methods Unsafe on every role — see
--rpc-methods does nothing
and Ports.
5. Generate session keys
The node generates the keys, and with them the proof of possession the chain
demands in the next step. Both are bound to your validator account, so the node
needs that account's public key: a 0x string of 64 hex characters, the 32 raw
bytes behind the SS58 address.
docker exec orbinum-validator orbinum-node key inspect <your SS58 address>
Copy the Public key (hex) line and pass it as the owner:
docker exec orbinum-validator curl -s -H 'Content-Type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"author_rotateKeysWithOwner","params":["<your account hex>"]}' \
http://localhost:9944
The result has two fields:
{"jsonrpc":"2.0","id":1,"result":{"keys":"0x…","proof":"0x…"}}
keys— 128 hex characters, 64 bytes: your Aura sr25519 public key followed by your GRANDPA ed25519 public key, in the order the runtime declares them.proof— 256 hex characters, 128 bytes: one signature per key over your account id. It is how the chain checks that the node registering the keys actually holds their private halves.
The private halves were written into the node's keystore at
/data/chains/orbinum_testnet/keystore and never left the server. Only keys
and proof go on-chain.
If the response has no proof field, your node is still syncing and executing
a runtime older than the one live on the chain. Wait until system_health
reports "isSyncing": false and run it again.
Calling author_rotateKeysWithOwner again generates a new pair and both
values you saved become stale. If you rotate after registering, you must submit
session.setKeys again with the new pair or your node stops authoring at the
next session.
Copy both values. You need them in the next step.
6. Submit session.setKeys
This is an on-chain extrinsic signed by your validator account — the account you will send to the Orbinum team, and the same one you passed as owner in step 5. It needs a balance to pay its fee: fund the account from the faucet first.
On Orbinum, ValidatorId is the AccountId directly. Your validator account is
an ordinary account and needs no relationship to your Aura key.
You may have read that a validator's account is its sr25519 key — that holds only for the genesis validators, whose accounts were derived from their Aura keys when the chain was built. A validator added later registers session keys under whatever account it already controls.
- Open Polkadot.js Apps and connect it to
wss://rpc-1.testnet.orbinum.io. - Go to Developer → Extrinsics.
- Select your validator account, then
session→setKeys(keys, proof). - Paste
keysfrom step 5 intokeys. - Paste
prooffrom step 5 intoproof. - Submit and sign.
The runtime verifies proof on-chain: each session key must have signed the id of
the account submitting the extrinsic. The call is rejected with
Session.InvalidProof when
proofis0x00or empty. Runtimes beforespec_version11 ignored it; the current one does not.- the account signing
setKeysis not the owner you passed toauthor_rotateKeysWithOwner. - you rotated again after copying, so
keysandproofno longer match.
In every case the fix is the same: redo step 5 with the right account hex and
resubmit with the new keys and proof.
Verify it landed. In Developer → Chain state, query
session.nextKeys(yourAccount). It must return the same keys you submitted. If
it returns nothing, the extrinsic did not finalize and the next step will fail.
addValidator is rejected with NoSessionKeys unless this extrinsic has already
finalized. Do not email the team before session.nextKeys returns your keys.
7. Confirm the keystore matches the chain
session.nextKeys proves the chain knows your keys. It does not prove your
node holds the private halves — the two come apart if you ran author_rotateKeysWithOwner
on a different machine, or recreated the container without its volume. Ask the
node itself:
docker exec orbinum-validator curl -s -H 'Content-Type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"author_hasSessionKeys","params":["<your keys>"]}' \
http://localhost:9944
Pass keys from step 5 exactly as author_rotateKeysWithOwner returned it — one 0x
string, Aura and GRANDPA concatenated, no separator.
{ "jsonrpc": "2.0", "result": true, "id": 1 }
true means this node can sign for the keys registered on-chain, and you are
done. false means it cannot: the keystore is missing them, and the node will
never author a block no matter how long you wait or how many sessions pass. Go
back to step 5 and rotate on this node, then submit setKeys again with the
new keys and proof.
false is silentNothing in the logs, on telemetry, or in validatorSet.approvedValidators reveals
this mismatch. The node syncs, peers, and looks healthy — it simply skips every
slot it is scheduled for. Checking here costs one command; skipping it costs two
session rotations before the symptom appears.