Guides / running-a-validator.md

Running a validator

A validator reads each miner's on-chain commitment, downloads and grades the recipe in an isolated sandbox, ranks passing miners by measured FHE latency, and sets geometric weights. It runs as two separate processes sharing one score-cache file:

  • the score process evaluates commitments and writes the cache (evaluations can take minutes — nothing else waits on them);
  • the weights process reads the cache, ranks, and calls set_weights on the chain's cadence — never blocked behind a long compile.

cifra validator run starts both automatically. Everything runs in .venv; all FHE work is shelled into the sandboxed .venv-fhe evaluator. Scores persist across restarts in the cache file.

Prerequisites

  • Both venvs built: make venvs
  • A registered validator hotkey with enough stake to set weights (a validator permit)
  • The exact pinned toolchain. Your concrete-ml / concrete-python versions must match the task's pin, or the validator will self-exclude from scoring (see below). The venvs install the pinned versions by default (concrete-ml==1.9.0, concrete-python==2.10.0).
  • The task repo id (same one miners use, e.g. cifranet/cifra-task)

Configure

Wallet, network, and task come from flags or .env; the sandbox budget (eval_timeout_s) is env-only — see configuration.md. A validator needs no HuggingFace token (downloads are anonymous). The weight-setting cadence is chain-enforced (weights_rate_limit, typically ~100 blocks) — there is no local knob.

Run

.venv/bin/cifra validator run \
  --netuid <N> --network test \
  --wallet.name <coldkey> --wallet.hotkey <hotkey> \
  --task cifranet/cifra-task
# add --once to run a single scoring round + one weight pass and exit

run spawns the weights process alongside the score process (log lines are prefixed [validator] and [weights]). To run them as two separate services instead, start cifra validator run on one and cifra validator weights on the other — they only share the cache file.

What it does (roles/validator.py)

At startup the validator loads its score cache ($CIFRA_CACHE_DIR/validator_cache.json) — verdicts and copy-priority ordering survive restarts. Then, every cycle of the score process:

  1. Sync the task. Check the task repo's head revision; if the owner published a new task, re-download it (every miner is then re-evaluated against the new task).
  2. Check every commitment. get_commitment(uid){repo, rev, block} (the commit block comes from the chain itself). Skip miners that committed nothing; skip evaluation when the cache already has a verdict for this (commitment, task) pair.
  3. Evaluate new commits in the sandbox (sandbox/jail.pypython -m cifra.fhe.evaluate): load without pickle, compile under a forced 128-bit configuration, run the accuracy gate, time real fhe="execute" inferences. Both latency and complexity are logged per miner. A hang / OOM / crash / timeout becomes a failed verdict, never a crashed validator.
  4. Save the cache ({score, repo, rev, task} per hotkey, atomic write).

Meanwhile the weights process, each cycle:

  1. Sets weights when the chain allows (blocks_since_last_update ≥ weights_rate_limit, ~100 blocks): reload the cache, filter to the current task, apply copy-priority, rank by latency — latencies within 3% tie, and within a tie the earlier committer wins — assign geometric weights 1, 1/3, 1/9, … down the leaderboard, log the full normalized weight vector, and submit with 3 attempts.

Set CIFRA_LOG_IO=true for a verbose per-recipe trace (latency, complexity, PBS count, accuracy, achieved security, wall-clock).

Toolchain matching

Complexity is only identical across validators within one pinned toolchain build. The evaluator checks its concrete-ml / concrete-python versions against the task's pin and excludes the validator from scoring on mismatch — a drifting validator goes quiet rather than emitting divergent numbers that would fork consensus. If your validator is scoring nothing, check its versions against the task's concrete_ml_version / concrete_python_version first.