1 → 50 validators on Railway
The chain's genesis already has max_validators = 50. Today there is one active validator (the genesis bootstrap). This page covers how to go from 1 → 50 on Railway specifically: what to deploy, what not to use, and how the services peer together.
The single most important rule
Validators are separate Railway services, NOT replicas.
| Replica (×N copies of one service) | Separate services | |
|---|---|---|
Share priv_validator_key.json? | Yes — same env, same volume → same key | No — each has its own |
| Outcome at consensus time | All copies sign the same block → double-sign | Each signs independently |
| Slashing | slash_fraction_double_sign = 5% of stake, tombstoned forever | Safe |
| Verdict | NEVER | THIS |
Replicas are fine for read-only RPC nodes (no signing key). They are catastrophic for validators.
Architecture for 50 validators on Railway
┌─────────────────────────────────────────┐
│ Railway project │
│ │
┌──────┐ │ ┌──────┐ ┌──────┐ ... ┌──────┐ │
│ user │────┼──│ val1 │ │ val2 │ │ val50│ │
│ / │ │ │ :pub │ │ │ │ │ │
│ dApp │ │ │ │←─┤ │←─────→│ │ │
└──────┘ │ └──┬───┘ └──────┘ p2p └──────┘ │
│ │ private network (26656) │
│ │ (*.railway.internal hostnames) │
│ │ │
│ ┌──▼───────┐ │
│ │ rpc-pool │ N replicas (read-only) │
│ │ :pub │ ← horizontally scaled │
│ └──────────┘ public RPC traffic │
└─────────────────────────────────────────┘- 50 validator services (
val1…val50). Each:- Its own
priv_validator_key.json(unique signing key). - Its own attached Volume at
/data. - Talks p2p with the others over Railway's private network (
<service>.railway.internal:26656— no public TCP exposure needed). - One or two of them may have a public domain for emergency RPC; the rest can be internal-only.
- Its own
rpc-pool— one service with many replicas. Read-only full nodes. Public domain serves user/dApp JSON-RPC. Replicas are correct here because no node in this pool signs blocks.
Step-by-step: add validators 2 through 50
Validator 1 is your current service (the genesis bootstrap). For each of val2 … val50 (do them one at a time, verify each before the next):
1. Create a new Railway service
- Railway → + New → GitHub Repo → this repo. Same
docker/Dockerfile. - Volume mounted at
/data(mandatory). - Settings → Variables:
CHAIN_ID=sanect_76287-1 MONIKER=val2 # change per service HOMEDIR=/data/.sanect - DO NOT Generate a public Domain yet (you don't need internet exposure to peer; private networking handles it). Optionally generate one later if you want this validator to also serve public RPC.
2. Make the new node join the existing chain (not start a new one)
The default entrypoint generates a fresh genesis on empty volumes. To join the running chain instead, before the node starts you need to:
a. Get the genesis of the running chain from val1's public endpoint:
curl https://rpc.testnet.sanect.com/rpc/genesis \
| jq -r .result.genesis > /tmp/genesis.jsonb. Get val1's node id (use this for peering):
curl -s https://rpc.testnet.sanect.com/rpc/status \
| jq -r .result.node_info.id
# -> e.g. 2880a2dc1dd6df403fc3b5637e2ada943abceaddc. Pre-seed the volume: there are two ways on Railway —
- Easy: SSH into the running service via Railway CLI, copy the genesis and persistent_peers into the volume before first start.
- In repo: commit
genesis.jsontodocker/network/genesis.jsonand add anentrypointflag likeJOIN_NETWORK=truethat copies it into the volume on first boot and writesseeds = "<val1_id>@val1.railway.internal:26656"intoconfig.toml. This helper is on the roadmap.
For now, the SSH-and-paste path is fastest.
d. persistent_peers for valN should be every other validator:
persistent_peers = "<val1_id>@val1.railway.internal:26656,<val2_id>@val2.railway.internal:26656,..."3. Wait for state sync, then create the validator
Once the new node catches up to the tip (catching_up: false in /rpc/status), submit MsgCreateValidator from a delegator account that holds enough SNCT to land in the top 50. See Run a validator → Path B.
4. Verify
curl https://<val1-public>/rpc/validators | jq '.result.validators | length'
# -> should grow toward 50 as you add validators.The number of active validators is whatever rank-fits in max_validators=50 by stake. Below-the-line validators (rank 51+) are "bonded but not active" until they outstake someone in the top 50. Registrations are unlimited — the waitlist rotates in automatically as stake shifts.
RPC replicas (the safe replica use case)
For public read-only RPC, replicas make sense — load is shared, no signing key involved:
- + New service → same repo → Settings → Replicas = 3 (or more).
- Each replica needs the chain data; CometBFT supports state sync so they catch up in minutes, not hours. Add to
config.toml:toml[statesync] enable = true rpc_servers = "https://rpc.testnet.sanect.com/rpc,..." trust_height = <recent_height> trust_hash = "<hash_at_that_height>" - Generate one Domain on the service; Railway load-balances across replicas.
- Crucially: this service must NOT have a validator key. The default entrypoint generates one anyway (every fresh sanectd init creates a
priv_validator_key.json), but as long as noMsgCreateValidatorever bonds those keys to stake, they're inert. To be safe, delete the file at the end ofentrypoint.shfor RPC-mode containers (env-gated:ROLE=rpc→ remove the key).
Sanity-check questions before you go
- "Do my 50 validator services each have their own Volume?" → Yes.
- "Do any of my validator services have replicas > 1?" → No.
- "Is at least 2/3 of voting power online at any time?" → BFT liveness needs this. With 50 validators that's 34 online for the chain to commit blocks.
- "Are validators in the same Railway region?" → Co-locate for the ~400 ms block target. Cross-region RTT alone can blow the consensus budget.
TL;DR cheat sheet
| Goal | What to deploy on Railway |
|---|---|
| Add an active validator | A new service (not a replica). Own volume, own key, peer via *.railway.internal. |
| Scale public RPC throughput | More replicas of the RPC service. Same image, no signing key bound to stake. |
| Bootstrap a fresh 50-validator chain | 50 services pre-seeded with a shared genesis.json containing 50 gentxs, started together. |
| Run a sentry / load shield | A new service behind the validators (full node, public, no key bonded), forwarding p2p to the private validators. |
Beyond Railway: multi-cloud validator sets
For real liveness against single-provider outages, validators are spread across multiple cloud providers in the same region (still co-located for the 400 ms block budget). This is the operator's playbook, kept inside the repo (not on this public docs site):
operators/multicloud-deploy.md— 50 validators across AWS / GCP / DigitalOcean / Vultr / Linode / OVH / Railway in Singapore, with per-provider commands.operators/keys-and-slashing.md— how to handlepriv_validator_key.jsonandpriv_validator_state.jsonso you don't lose 5% to a double-sign.operators/runbook.md— jailed validator, halted chain, provider outage, upgrades, recovery.