Skip to content

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 keyNo — each has its own
Outcome at consensus timeAll copies sign the same block → double-signEach signs independently
Slashingslash_fraction_double_sign = 5% of stake, tombstoned foreverSafe
VerdictNEVERTHIS

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 (val1val50). 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.
  • 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:

bash
curl https://rpc.testnet.sanect.com/rpc/genesis \
  | jq -r .result.genesis > /tmp/genesis.json

b. Get val1's node id (use this for peering):

bash
curl -s https://rpc.testnet.sanect.com/rpc/status \
  | jq -r .result.node_info.id
# -> e.g. 2880a2dc1dd6df403fc3b5637e2ada943abceadd

c. 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.json to docker/network/genesis.json and add an entrypoint flag like JOIN_NETWORK=true that copies it into the volume on first boot and writes seeds = "<val1_id>@val1.railway.internal:26656" into config.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

bash
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:

  1. + New service → same repo → Settings → Replicas = 3 (or more).
  2. 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>"
  3. Generate one Domain on the service; Railway load-balances across replicas.
  4. 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 no MsgCreateValidator ever bonds those keys to stake, they're inert. To be safe, delete the file at the end of entrypoint.sh for 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

GoalWhat to deploy on Railway
Add an active validatorA new service (not a replica). Own volume, own key, peer via *.railway.internal.
Scale public RPC throughputMore replicas of the RPC service. Same image, no signing key bound to stake.
Bootstrap a fresh 50-validator chain50 services pre-seeded with a shared genesis.json containing 50 gentxs, started together.
Run a sentry / load shieldA 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):