Design / architecture.md

Architecture

Cifra is a Bittensor subnet where miners compete to build the fastest fully-homomorphic circuit for a model the owner publishes. This is the engineering rationale; the whitepaper is the narrative version.

The three roles

Role Runs in Does
Owner Publishes a plaintext reference model + calibration + rules (accuracy floor, complexity cap, p_error floor, toolchain pin) to a HuggingFace repo. Sets no weights.
Miner .venv (+ shells .venv-fhe) Compiles its own optimized circuit for the model, uploads the recipe to HuggingFace, commits {repo, rev} on-chain.
Validator .venv (+ shells .venv-fhe) Downloads each recipe, compiles + grades it in a sandbox, ranks passing miners by measured FHE latency, sets geometric weights.
owner  ──publish──►  reference model + calibration + rules
miner  ──compile──►  recipe.json ──HF upload──► set_commitment({repo, rev})
validator ─get_commitment─► download ─► SANDBOX(compile 128-bit → accuracy gate → latency + complexity)
          ─► latency rank (3% tie-band, copy-priority) ─► geometric weights ─► set_weights

Why these choices

Rank by measured latency; gate deterministically. The competitive metric is the measured fhe="execute" latency of the circuit (mean ms per encrypted inference — keygen excluded), because latency is what the owner ultimately buys. Latency is a local measurement, so the design keeps consensus tight in three ways:

  • Only the ordering matters. Weights are geometric down the leaderboard (1, 1/3, 1/9, …), so validators need only agree on ranks, not raw milliseconds.
  • A 3% tie-band absorbs jitter. Same-machine repeat timings vary ~1–2%, while real circuit improvements shift latency by tens of percent. Latencies within 3% of the band leader count as equal, and within a band the earlier on-chain committer wins (the commit block is read from the chain, identical on every validator). A challenger must be genuinely >3% faster to displace an incumbent — which also makes copy-and-tweak leapfrogging pointless.
  • The gates stay fully deterministic. Eligibility (accuracy floor, forced 128-bit, complexity band) uses circuit.complexity and a seeded gate set — identical everywhere under the pinned toolchain.

complexity is still measured and logged next to latency (it is the deterministic, hardware-neutral proxy), and the caveat is stated plainly: validators on very different hardware can disagree on adjacent ranks; keeping validator hardware roughly uniform keeps the ranking stable. See core/scoring.py.

The validator compiles, not the miner. A recipe is a description — Concrete-ML model JSON loaded via loaders.loads, never pickle/joblib — so it carries no executable code. The validator builds the compile Configuration itself, which buys two guarantees:

  • 128-bit security is structural. The validator forces security_level = SECURITY_128_BITS, and Concrete's enum has no weaker rung — so "weak-crypto-for-speed" cannot be requested. The verdict re-checks the achieved level. See fhe/compile.py.
  • No RCE. No miner binary ever runs on the validator. Compilation happens in an isolated subprocess anyway (defense in depth): CPU + wall-clock bounded, credentials stripped. See sandbox/jail.py and fhe/evaluate.py.

The accuracy gate is deterministic. Gate inputs are seeded from the task_id (core/sampling.py), so a miner's self-check and every validator draw the identical set — the pass/fail boundary is reproducible.

Copy-priority. Recipes are fingerprinted (SHA-256 of the canonical envelope); the earliest on-chain committer of a fingerprint keeps the reward, later copies score zero. See apply_copy_priority in core/scoring.py.

Toolchain pin. Deterministic complexity holds only within one concrete-ml / concrete-python build. The task pins both versions; a validator whose versions differ excludes itself from scoring rather than emit divergent numbers.

The two-venv split

bittensor needs numpy 2.x; concrete-ml pins numpy 1.26.4 — they cannot share a venv. The split is also the security boundary: the chain process (which holds the wallet) never imports concrete-ml and never runs miner-influenced code; it only shells into the isolated .venv-fhe evaluator and reads back a small JSON verdict. The one rule: no module imports both bittensor and concrete-ml.

cifra/
  core/      pure numpy — recipe envelope, scoring, sampling (safe in both venvs)
  chain/     the only bittensor import — metagraph, get/set_commitment, set_weights
  hub/       HuggingFace upload/download (rev-pinned, byte-capped)
  fhe/       concrete-ml — compile (128-bit), sandbox evaluator, miner optimizer
  sandbox/   isolate the evaluator subprocess (CPU + wall-clock bounded)
  roles/     miner / validator orchestration + task loading (from a HF repo id)
  cli.py     `cifra miner|validator ...`

See also