Validator Quickstart
Six-step setup for a Minotaur validator on Bittensor Subnet 112 — hardware, prerequisites, on-chain ValidatorRegistry handshake, canonical Docker stack, and peer discovery.
This guide walks you through setting up and running a Minotaur validator on Bittensor Subnet 112. By the end you’ll have a registered hotkey, an EVM signing address in the on-chain ValidatorRegistry, the canonical Docker stack running, and your /identity endpoint discoverable by other validators.
Hardware requirements
Every validator must be leader-capable. Leadership on Subnet 112 is awarded by stake at the metagraph level — the highest-stake validator runs the leader role, ties broken by hotkey lexicographic order. Leadership rotates whenever stakes shift on chain, with no advance notice. A validator that cannot immediately accept the leader role on stake change is a free-rider.
Baseline (today)
Subnet 112 currently operates one App (DexAggregatorApp) across two real chains (Base + BT EVM, Ethereum in Phase 4). User volume is light.
| Spec | Value |
|---|---|
| vCPU | 4 (modern x86_64) |
| RAM | 8 GB |
| Storage | 100 GB SSD/NVMe |
| GPU | none |
| Network out | ~100 GB/month |
| Public IPv4 | yes — ports 9100/tcp and 8080/tcp must be reachable |
Steady-state at this spec uses ~4–5 GB RAM and well under 1 vCPU. Re-simulation bursts (when the leader proposes a new order) peak around 6–7 GB RAM and 2–3 vCPU. 8 GB / 4 vCPU leaves a healthy margin.
Growth path
| Trigger event | Recommended spec |
|---|---|
| Baseline (today) | 4 vCPU / 8 GB / 100 GB SSD |
| +1 chain added (one extra Anvil fork = ~1.5–2 GB RAM) | 4 vCPU / 12 GB / 150 GB SSD |
| 2+ new chains, or sustained user volume making JS scoring run continuously | 8 vCPU / 16 GB / 200 GB SSD |
| Multi-app phase (several Apps live, each with independent benchmarks) | 8 vCPU / 32 GB / 200 GB SSD |
Scaling is vertical — no horizontal sharding needed at the validator level. Plan to upsize at the announcement of each new chain integration.
Required maintenance cron CRITICAL
Anvil’s overlay filesystem grows fast — production deployments measure ~40–50 GB per fork per day. With three forks that’s ~150 GB/day of bloat. Without a frequent recycle, a 100 GB volume fills in well under a day and the host OS hangs once the disk hits 100% — SSH is dead and the only recovery is a force stop/start at the hypervisor.
Install this cron — every 6 hours, not daily:
0 */6 * * * root docker compose -f /opt/minotaur/platform/validator/docker-compose.yml \
rm -fsv anvil anvil-base anvil-btevm && \
docker compose -f /opt/minotaur/platform/validator/docker-compose.yml \
up -d anvil anvil-base anvil-btevm Each recycle window drops in-flight Anvil state for ~60 seconds. If you’re running a high-stake validator, stagger your cron a few minutes from peers to avoid simultaneous reorg pauses.
SSH is dead. docker compose down won’t help. The only recovery is a force stop/start at the hypervisor layer (AWS: aws ec2 stop-instances --force --instance-ids <id>, wait for stopped, then start-instances). EBS-backed instances preserve state. Once back, immediately docker compose rm -fsv the anvil services to release their snapshot overlays — docker system prune alone won’t reclaim them.
Third-party APIs
| Provider | Used for | Free tier sufficient? |
|---|---|---|
| Alchemy / Infura (Ethereum) | Source RPC for the Anvil ETH fork; archive endpoint needed | Yes for moderate load. Premium recommended once you take leader for non-trivial periods. |
Alchemy / Infura (Base, chain 8453) | Source RPC for the Anvil Base fork | Same as above, same account works |
| Public BT EVM RPC | https://lite.chain.opentensor.ai (chain 964) | Public endpoint, no signup |
| Public Finney WS | wss://entrypoint-finney.opentensor.ai:443 — metagraph reads | Public endpoint, no signup |
| GitHub API (read-only) | Cloning miner submissions during leader benchmarking | Anonymous works for small subnets; provision a PAT before you take leader to raise rate limits |
No GPU compute or LLM API is required. The JS scoring engine is pure Node.js, deterministic, CPU-bound.
Ports
Inbound (must be reachable from the public internet)
| Port | Service | Notes |
|---|---|---|
9100/tcp | Validator daemon — order-consensus signing + /identity | Reached by the current leader for proposal broadcast and by peers for discovery. |
8080/tcp | API service — champion-consensus signing + /identity | Reached by the current leader for champion-certification proposals. |
sudo ufw allow 9100/tcp comment "minotaur validator daemon"
sudo ufw allow 8080/tcp comment "minotaur api service" For AWS security groups, allow inbound TCP 9100 and 8080 from 0.0.0.0/0.
Outbound (egress, no special configuration)
| Destination | Port | Purpose |
|---|---|---|
entrypoint-finney.opentensor.ai | 443 (WSS) | Subtensor metagraph reads |
lite.chain.opentensor.ai | 443 (HTTPS) | BT EVM RPC |
| Alchemy / Infura | 443 (HTTPS) | ETH + Base fork source RPCs |
| Other validator peers | 9100 (HTTPS) | Order-consensus signing |
| Leader API host | 8080 (HTTPS) | Proposal pull (followers) |
github.com, ghcr.io | 443 (HTTPS) | Image and repo pulls |
Internal only (must NEVER be exposed)
Anvil ports (8545 ETH, 8546 Base, 8547 BT EVM) are bound to the Docker network. Do not expose them to the public internet.
Prerequisites
Two host-level tools:
- Docker (≥25, for Watchtower compatibility) + Docker Compose plugin
- Foundry (
cast) — used to generate your EVM signing key and read on-chain state - Bittensor CLI (
btcli) — register your hotkey on subnet 112
And account-level things you provide:
- Bittensor wallet with a registered hotkey on subnet 112 (Step 3)
- EVM private key for EIP-712 consensus signing (Step 4 — a fresh key, does NOT hold funds)
- Ethereum + Base RPC URLs from Alchemy or Infura
- Coordination with the subnet operator to be added to the on-chain
ValidatorRegistry(Step 4)
Step 1 — Clone the repo
You only need the repo for the canonical compose file + the .env.example. The validator + api Python code runs inside the Docker image (no local venv / pip install needed):
git clone https://github.com/subnet112/minotaur_subnet.git
cd minotaur_subnet Step 2 — Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
anvil --version && forge --version && cast --version Step 3 — Register on Subnet 112
If your hotkey is not yet registered:
btcli subnet register --netuid 112 --subtensor.network finney \
--wallet.name my-validator --wallet.hotkey my-hotkey Verify registration:
btcli subnet metagraph --netuid 112 --subtensor.network finney Your hotkey should appear in the metagraph. Ensure you have sufficient TAO staked to participate in leader election.
Step 4 — Get onboarded to the on-chain ValidatorRegistry
Until your EVM signing address is added to the on-chain ValidatorRegistry, your consensus signatures will be ignored — you’ll collect emissions but contribute nothing.
Send the following three values to the registry owner (via the project’s GitHub issue template or current contact channel — check the project README):
Validator hotkey (SS58): 5...EVM signing address: 0x... (derived from your VALIDATOR_PRIVATE_KEY)Public axon URL: http://your-host:9100The owner runs, once per chain:
# Read the current set
cast call $VALIDATOR_REGISTRY 'getValidators()(address[])' --rpc-url $RPC_URL
# Add you (full new list, sorted ascending)
cast send $VALIDATOR_REGISTRY \
'updateValidators(address[])' \
'[0xExistingValidator1,0xExistingValidator2,0xYourEvmAddress]' \
--rpc-url $RPC_URL --private-key $REGISTRY_OWNER_KEY Repeat per chain (Base + BT EVM today; Ethereum in Phase 4 — addresses in the Network Reference).
Verify before continuing:
cast call $VALIDATOR_REGISTRY 'isValidator(address)(bool)' \
0xYourEvmAddress --rpc-url $RPC_URL If this returns true on every chain, you’re cleared. If false, your consensus signatures will be ignored.
Step 5 — Configure environment
The recommended path (Step 6) reads from platform/validator/.env. The variables below also work as shell exports if you’re running the daemon directly under systemd.
# Bittensor identity
WALLET_NAME=my-validator
HOTKEY_NAME=my-hotkey
NETUID=112
SUBTENSOR_URL=wss://entrypoint-finney.opentensor.ai:443
# Anvil forks the validator CONNECTS TO. It does NOT spawn them — Step 6
# brings them up as separate compose services.
ANVIL_RPC_URL=http://localhost:8545 # Ethereum fork
BASE_RPC_URL=http://localhost:8546 # Base fork
BITTENSOR_EVM_RPC_URL=http://localhost:8547 # BT EVM fork
# Upstream RPCs — used by the validator to advance each fork to the current
# chain head between simulations. Without these the fork stays frozen.
ETH_UPSTREAM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY
BASE_UPSTREAM_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY
BITTENSOR_EVM_UPSTREAM_RPC_URL=https://lite.chain.opentensor.ai
# Consensus signing
VALIDATOR_PRIVATE_KEY=0xYOUR_EVM_PRIVATE_KEY
# Public URL where your daemon serves /identity. Required for peer discovery
# — other validators sign and verify against this.
VALIDATOR_AXON_URL=http://your-public-host:9100
# On-chain ValidatorRegistry per chain — see the Network Reference.
VALIDATOR_REGISTRY_8453=0x88a08d1105393EACE9B6f5ff678DbE508B8639aC
VALIDATOR_REGISTRY_964=0x0B5fE44e90515571761D86C28c4855F325EDE098
CHAIN_ID=8453
LOG_LEVEL=INFO See Configuration for the full list of options and Quorum Management for changing the network-wide quorum value.
Step 6 — Bring up the validator stack
The validator role for a third-party operator is six containers:
- Three Anvil forks (Ethereum mainnet, Base, BT EVM)
- The validator daemon (order-consensus signer + weight emitter, port
9100) - The api service (champion-consensus signer, port
8080) - A docker-socket-proxy (limits the api’s Docker access to spawning sandboxed solver containers — used during reactive benchmarking)
Plus an opt-in seventh container: Watchtower, for auto-update on :stable tag promotion.
The canonical compose ships at platform/validator/:
cd platform/validator
cp .env.example .env
$EDITOR .env # fill in every YOUR_* placeholder
docker compose --profile autoupdate up -d # with Watchtower auto-update
# ── or ──
docker compose up -d # without (manual pulls) The first cold start takes ~60–90 seconds while the three Anvil forks fetch initial state from the upstream RPCs.
Check the state:
docker compose ps # all six should be (healthy) after ~90s
docker compose logs -f validator api You should see:
validator | ProtocolConfig: loaded quorum_bps=6666 from ValidatorRegistry ...validator | Consensus enabled (id=0x..., peer-mode=discovered, quorum=6666 bps)validator | Validator starting as ORDER CONSENSUS PEER ...validator | BlockLoop started (tick_interval=12.0s)api | Champion consensus enabled (validator=0x..., quorum=N bps from ChampionRegistry 0x..., validator-set from VR 0x... on chain 964)Verify
The bundled script runs every endpoint + checks registry state:
bash scripts/check_validator.sh Or manually:
# Validator health (order-consensus)
curl http://localhost:9100/health
# Api health (champion-consensus)
curl http://localhost:8080/health
# Quorum read from chain
curl http://localhost:9100/consensus/info
# /identity served on both ports (peer-discovery cross-checks both)
curl http://localhost:9100/identity
curl http://localhost:8080/identity If /consensus/info or the api’s champion_consensus.quorum_required looks wrong, your VALIDATOR_REGISTRY_* envs may point at a stale contract — check the Network Reference.
What each service does
| Service | Role |
|---|---|
validator | Order-consensus signing (re-simulates leader’s order proposals on Anvil, signs approvals if both JS + on-chain scores meet threshold). Emits weights every ~20 min. |
api | Champion-consensus signing (reactively re-benchmarks the leader’s champion candidate inside a sandboxed Docker container, signs the certification if scores hold up). |
anvil-eth, anvil-base, anvil-btevm | Local forks of each chain so independent re-simulation doesn’t touch mainnet. |
docker-socket-proxy | Filtered Docker socket access for the api service. Only CONTAINERS + IMAGES + NETWORKS + POST allowed. |
watchtower (opt-in) | Polls GHCR every hour for a new :stable SHA; recreates validator + api when the tag moves. |
What this stack does NOT include
- Relayer: the transaction submitter is a singleton operated by the subnet team. Your validator signs proposals; the leader’s submission path uses the team’s relayer. You never deal with a gas wallet.
- Order/champion proposing: only the highest-stake validator’s leader-api does that. As a follower, you receive proposals, verify, and sign.
Peer discovery
Order-consensus and champion-consensus peers are both discovered automatically — no peer-list env required for either.
The flow:
-
Your daemon publishes its identity
GET /identityon both:9100and:8080returns a fresh EIP-712 signed payload binding(evm_address, hotkey, axon_url). Each request regenerates the signature so it’s never stale. -
You publish your axon URL
Set
VALIDATOR_AXON_URL=http://your-host:9100in the daemon env. The signed payload includes this. Also usebtclito register your axon on the Bittensor metagraph. -
Other validators discover you
Their
ProtocolConfig.refresh_loop(default 60s tick) walks the metagraph axon list, probes each/identity, verifies the EIP-712 signature, and cross-checks the recovered EVM address is inValidatorRegistry.getValidators()and the hotkey matches the metagraph.
What this gives operators:
- No coordinated restart when a new validator joins — others pick them up within one refresh tick.
- No peer-list env to maintain across the cluster.
- IP changes are self-served — update
VALIDATOR_AXON_URL, restart, the signed payload re-publishes automatically.
Required env for discovery
| Variable | Purpose |
|---|---|
VALIDATOR_AXON_URL | Public URL serving /identity, typically http://<public-ip>:9100. Signed into the payload — if missing, /identity returns 503. |
SUBTENSOR_URL | Required to read the metagraph for axon discovery. |
VALIDATOR_REGISTRY_ADDRESS | Required to read the authorized EVM set. |
ORDER_CONSENSUS_PEERS and CHAMPION_CONSENSUS_PEERS are pinned-peer escape hatches used by the subnet team’s own production deployment (where metagraph axons aren’t published yet for operational-security reasons). Both bypass automatic discovery and lock you to a stale list — your validator becomes invisible to other peers. Always leave them unset.
Auto-update
The default MINOTAUR_IMAGE_TAG=stable (in .env.example) and the optional Watchtower container give hands-off updates:
- New commit lands on
main→:latestand:sha-<short>pushed to GHCR. - The new image runs on the subnet team’s prod for a soak period.
- The team runs
promote-stable.ymlto re-tag:sha-<short>as:stable. - Your Watchtower polls GHCR within the next hour, pulls the new image, recreates
validator+api. ~30–60s downtime during the recreate.
Manual control instead of Watchtower:
docker compose pull validator api
docker compose up -d --force-recreate validator api Pin a specific SHA (opt out of auto-update entirely without removing Watchtower):
MINOTAUR_IMAGE_TAG=sha-abc1234 Running without Docker
If you prefer Anvil under systemd plus the daemon as a native process:
anvil --host 0.0.0.0 --port 8545 --fork-url "$ETH_UPSTREAM_RPC_URL" --block-time 2
anvil --host 0.0.0.0 --port 8546 --fork-url "$BASE_UPSTREAM_RPC_URL" --chain-id 8453 --no-storage-caching --block-time 2
anvil --host 0.0.0.0 --port 8547 --fork-url "$BITTENSOR_EVM_UPSTREAM_RPC_URL" --chain-id 964 --no-storage-caching --block-time 2
python -m minotaur_subnet.validator.main \
--port 9100 \
--netuid 112 \
--wallet-name "$WALLET_NAME" \
--hotkey-name "$HOTKEY_NAME" \
--subtensor-url "$SUBTENSOR_URL" \
--validator-key "$VALIDATOR_PRIVATE_KEY" \
--tick-interval 12.0 \
--epoch-seconds 1200 Wrap each in its own systemd unit with Restart=on-failure. The Anvil disk-bloat cron above applies either way — adjust it to bounce your systemd units instead of docker compose rm -fsv.
[Unit]
Description=Minotaur Validator Daemon
After=network-online.target
[Service]
EnvironmentFile=/etc/minotaur/env
ExecStart=/opt/minotaur/.venv/bin/python -m minotaur_subnet.validator.main --port 9100 --epoch-seconds 1200
Restart=on-failure
RestartSec=5
User=minotaur
[Install]
WantedBy=multi-user.target Local testnet (development only)
For local development of the protocol itself, the full stack ships in Docker Compose:
cd platform/local_testnet
cp .env.example .env
# Edit .env and set ALCHEMY_RPC_URL and BASE_ALCHEMY_RPC_URL
make testnet-up Local testnet services
| Service | Port | URL |
|---|---|---|
| API | 8080 | http://localhost:8080 |
| Validator | 9100 | (internal, via Docker network) |
| Relayer | 8091 | http://localhost:8091 |
| Anvil (ETH fork) | 8545 | http://localhost:8545 |
| Anvil (Base fork) | 8546 | http://localhost:8546 |
| Anvil (BT EVM fork) | 8547 | http://localhost:8547 |
| Subtensor | 9944 | ws://localhost:9944 |
The init container automatically registers the subnet (netuid=1 on local), registers validator and miner neurons, and deploys contracts. The validator starts with FORCE_LEADER=1.
Stop with make testnet-down.
Running tests
make test # unit + app tests (no Docker/Anvil needed)
make test-all # full suite including emulation and E2E
make test-testnet # live local_testnet smoke (recreates the Docker stack first)
make test-e2e # E2E tests (requires Foundry/Anvil)
make test-fork # mainnet-fork-only E2E (requires ALCHEMY_API_KEY) make test-all does not include make test-testnet — keep the latter as a separate live-stack check when you change API, deployment, wallet, quoting, or order execution flows.