Anti-Cheat
The miner owns the machine, samples its own tasks, runs its own benchmarks, and reports its own score. Every honest-looking number in the system is produced by someone with a direct financial interest in inflating it. This page is the complete account of why that is nonetheless safe — and where it still isn't.
The design principle: because the artifact is public and cryptographically bound to what ran, cheating is publicly detectable rather than something we must prevent by hiding data.
The attack surface, and what stops each attack
| Attack | Backstop | Reason emitted |
|---|---|---|
| Run code B, claim artifact A's score | The runtime hashes the bytes it loads and runs; the validator re-hashes the bundle it downloads | artifact_binding_mismatch |
| Edit the proof after attestation | report_data commits to every field; the quote breaks |
report_data_mismatch |
| Forge the enclave | Full DCAP to a pinned Intel root | bad_platform_quote |
| Run a modified runtime | MRTD + RTMR1/2/3 gated against the owner's on-chain set | unapproved_runtime |
| Run on a vulnerable platform | TCB status policy | tcb_out_of_date, tcb_revoked |
| Under-report cost | Metered in-enclave, agent has no network and no key | — (impossible) |
| Answer without the pool | Trace inspected for ≥ 1 metered call per task | no_pool_call |
| Publish a trace that doesn't match | Trace hashed into call_log_hash, bound by the quote |
trace_mismatch |
| Reuse an old good proof | Proof binds (epoch, nonce) |
epoch_nonce_mismatch |
| Run 20 times, submit the best | One nonce per epoch, seeded by an unpredictable beacon | epoch_nonce_mismatch |
| Grind within the epoch / swap the revealed proof | (opt-in) Intra-epoch commit window (F7): commit report_data on-chain in-window, reveal exactly it |
no_proof_commit · commit_out_of_window · commit_mismatch |
| Front-run a rival's commit | Commit-reveal delay | — |
| Copy a rival's commit string | Commit is salted with your hotkey | bad_commit |
| Resubmit a rival's proof | Proof binds the hotkey | hotkey_mismatch |
| Copy a rival's artifact | Behavioural dedup on the attested answer vectors; earliest commit wins | copy_of:<uid> |
| Hardcode answers | Source is public and scanned | hardcoded_answers |
| Memorise the benchmark (table in the opaque weights) | Grounding check (default): every answer must derive from a logged pool call. Opt-in: secret held-out probe + collapse test | ungrounded (default) · memorization (probe) |
| Call a stronger off-pool model | PinnedBackend allow-list |
UnpinnedModelError |
| Point the validator at nothing | Every audit fails closed | audit_error:*, no_proof |
The binding
This is the load-bearing mechanism, and the one an earlier adversarial review found vacuous before it was fixed.
The naive design has the miner supply source_hash and weights_hash alongside its results. That proves nothing — a miner runs code B and stamps artifact A's hashes. The fix has two halves, and both are required:
- The runtime derives the identity. The measured enclave
load_agents the committed bytes and computessource_hash/weights_hashfrom exactly those bytes. The miner never supplies them. So "what ran" ≡ "what's attested." - The validator binds to the commit. It downloads the public bundle itself, recomputes both hashes, and checks them against the on-chain commit via
verify_commit— never taking the miner's word.
One hardware signature over {measurement, report_data} then asserts both "this approved enclave ran" and "it produced this exact payload."
Commit-reveal and the hotkey salt
koth1|<hf_repo>|<revision>|sha256(source_hash|weights_hash|hotkey)
- The reveal delay stops front-running: nobody sees your artifact hashes before yours is committed.
- The hotkey salt stops commit-copying: the hash only verifies against your hotkey, so republishing a rival's commit string binds nothing.
- Earliest commit wins every tie, so being the copier never pays even when the copy is perfect.
The epoch nonce
The task slice is seeded from a per-epoch nonce derived from the block hash at epoch start:
nonce = sha256("koth-epoch|{epoch}|{beacon}")[:16]
seed = sha256("{nonce}|{epoch}|{benchmark}")[:8]
Nobody — including the owner — knows the slice until that block is minted. This is what simultaneously kills best-of-N (you get one slice per epoch, and it binds into the proof) and pre-computation (you cannot memorise a slice you cannot predict).
Behavioural dedup
A weight hash is trivially defeated: perturb a float and the hash changes. So copies are detected by behaviour, not bytes.
By default the fingerprint is the attested scored-answer vector already in each proof — no re-execution needed. Miners whose answer vectors agree at ≥ 95 % are clustered; the earliest-committed member is kept and the rest are disqualified as copy_of:<uid>.
This is robust to weight-space perturbation, prompt reformatting, and cosmetic source edits — anything that leaves behaviour intact.
Memorisation: the default is grounding
The threat is a router that hides an answer table in its opaque weights — invisible to the public-source scan. Two backstops address it, and the important one is now the default.
The grounding check (default). Every scored answer must match a pool response logged in the hash-attested trace. An agent that ignores the pool and returns a memorised answer from its own weights matches no logged response and is disqualified ungrounded once the ungrounded fraction exceeds a small slack (default 15 %, for synthesis orchestration + parser noise). This is pure proof-inspection: the validator reads only the proof and the trace, runs no miner code, and needs no secret data — deterministic and $0. It closes the total memoriser (the memorise-everything strategy) directly.
Its residual is honest and specific: prompt-injection laundering — the agent tells the pool model the memorised answer inside a legitimate prompt and the model echoes it back, so the answer is "grounded" in a pool response. That passes. The counterfactual upgrade is a null-pool re-execution, which is exactly what the opt-in probe adds.
The opt-in re-execution probe. Enabled with --probe-bank (audit_mode="probe"), the validator re-runs the downloaded bound artifact on a secret held-out slice and disqualifies memorization on a statistically significant collapse. Getting this right required a real correction, because a secret probe is not the same difficulty as the public slice:
The naive test — a one-sided two-proportion z-test (z_crit = 2.33) on claimed vs fresh — is unsafe once the probe is secret. The probe is drawn from an owner-curated bank the miner cannot derive, so it need not match the difficulty of the scored slice. An honest, perfectly-generalising router then shows claimed > fresh purely because the probe is harder; at n = 64, a 15-point difficulty gap false-DQs an honest miner — and the test's power grows with more probe data, so collecting more evidence makes it worse.
The fix is difficulty normalisation against the cohort:
allowance = median over this epoch's miners of (claimed_acc - fresh_acc) # capped at max_drop
flag if (claimed - fresh) exceeds allowance by more than sampling noise
- The median, not the mean, so a minority of genuine memorisers cannot inflate the allowance and hide behind it.
- Capped at
max_probe_drop(default0.25) — the owner's declaration of "no honest probe is harder than this" — so a fully colluding cohort of memorisers cannot raise the allowance enough to escape. - With fewer than
min_cohort(default3) miners there is nothing to calibrate against, so it falls back to that same owner-declared cap rather than to zero. An allowance of zero would apply the strict absolute test and false-DQ honest miners whenever the probe is harder than the public set.
The separation this buys: "the probe is hard" makes everyone drop; "this miner memorised" makes only them drop.
The probe bank is committed on-chain as probe_commit (a canonical, order-independent hash of the bank). In probe mode a validator without the matching bank refuses to score at all — probe_bank_missing, probe_bank_mismatch, probe_bank_unverified — rather than running a toothless audit.
The intra-epoch commit window (opt-in)
The epoch nonce kills cross-epoch best-of-N, but within a single epoch a miner could still grind many runs against the same slice and reveal the luckiest. Two landed, off-by-default refinements close the wall-clock gap:
- Commit window (F7). Each epoch the miner commits its proof's
report_data()on-chain inside[epoch·blocks, +W], then reveals the proof. The validator scores it only if the revealed proof matches the on-chain commit and the commit landed in-window. That binds one attested run — a post-commit best-of-N swap iscommit_mismatch, a late commitcommit_out_of_window, no commitno_proof_commit— and caps pre-commit grinding toW's wall-clock. The windowWis the actual lever; calibrate it to one suite run. - Grace window (F2). The validator scores a settled epoch (the latest whose grace deadline
(current_block − G) // blockshas passed), never the live one — so every miner had the full commit-and-upload window and two validators polling at different blocks compute the same result.
Both are off by default (the offline sim and single-validator testnet don't need them); enable them together on mainnet once W and G are measured.
Public source, and the fraud-proof path
Because the source is public, the cheapest audit is to read it. The validator pattern-scans the artifact it downloaded itself for literal answer tables → hardcoded_answers.
Anyone can also raise a challenge against a miner. The adjudicator binds first:
if hash_source(challenge.source_text) != committed_source_hash:
return (False, "unbound_source") # a griefer cannot fabricate source against an honest miner
return scan_source(challenge.source_text)
Not yet implemented: challenger stake and slash. Without it the fraud-proof path is free to spam, so it is advisory today.
Metering and no-egress
Cost cannot be under-reported because the agent never touches the network or the API key:
parent (trusted runtime) child (untrusted agent)
holds the pool key unshare --net → zero egress
meters every call ← call_model ← agent(prompt, call_model)
performs the real request → response →
records the authoritative trace tmpfs over secret dirs, rlimits
Metering is parent-side and therefore un-bypassable. The published trace is hashed into call_log_hash, which the quote binds — so the trace cannot be edited either.
Fail-closed by default
A validator that cannot verify something disqualifies; it does not skip. An unset gate is a configuration error, not a permissive default:
mock_quote_rejected · mrtd_gate_unset · rtmr_gate_unset · tcb_policy_unset · probe_bank_missing · probe_bank_mismatch · audit_error:*
A governance_ready() preflight refuses to start the daemon at all until the owner has published the approved-measurement set on-chain.
What is still open
Stated plainly:
- Grounding laundering. The default grounding check passes an answer the agent fed to the pool model and got echoed back (prompt-injection). The opt-in null-pool re-execution probe is the counterfactual upgrade for when real emissions attract a competent adversary.
- Prompt-based exfiltration. The agent can embed the nonce or task text in a legitimate prompt to the pinned model, reaching a confederate through the sanctioned channel. A network namespace cannot close this. Bounded by running the probe audit against a trusted pool.
- The measured image is not built. Until it is,
unapproved_runtimecannot distinguish your runtime from any other TDX guest on the same cloud, because a stock CVM'sMRTDis shared. This is the hard prerequisite for secure mainnet mining. See Attestation. - Probe-data curation. The opt-in secret bank must be sourced from tasks genuinely outside the pool models' training data. That is curation work, not code.
- Challenger stake/slash is unimplemented.
- The opt-in fresh-probe is statistical, not a proof. It bounds memorisation; it does not eliminate it.
- Side channels under a miner-owned host are out of scope, as is a full hardware break.
See also
- Attestation & Trust Model — what the hardware signature proves
- The KOTH Protocol — where each check sits in the validator loop