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_weightson 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-pythonversions 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:
- 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).
- 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. - Evaluate new commits in the sandbox (
sandbox/jail.py→python -m cifra.fhe.evaluate): load without pickle, compile under a forced 128-bit configuration, run the accuracy gate, time realfhe="execute"inferences. Both latency and complexity are logged per miner. A hang / OOM / crash / timeout becomes a failed verdict, never a crashed validator. - Save the cache (
{score, repo, rev, task}per hotkey, atomic write).
Meanwhile the weights process, each cycle:
- 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 weights1, 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.