This is the wire-level contract of a Hypertrain run: who signs what, which bytes get hashed, how a round moves through its states, and what a miner must pin so an auditor can reproduce its bits. Every statement here comes from src/hypertrain/protocol, challenge, auditor, ledger and trainer/determinism.py. The HTTP surface is in challenge-routes.md, the incentive math in mechanism.md.
Tagged numbers like 40{ev:docs/schemas/example-run-manifest.json#manifest.verify.T.audit_after_commit} are checked against the example manifest by scripts/check_doc_numbers.py.
Fifteen message types are signed. The JSON Schema of each is generated from the pydantic model (uv run --frozen python -m hypertrain.protocol.schema docs/schemas) and lives in docs/schemas/.
| Message | Sender | Schema | Purpose |
|---|---|---|---|
| RunManifest | run owner (K_owner) | RunManifest.json | Everything that defines a run. run_id = sha256(JCS(body)) |
| RoundOpen | K_coord / aggregator | RoundOpen.json | Roster, state hashes and the six drand deadlines of round w |
| Accept | miner | Accept.json | Takes the assignment, reports image digest, driver and GPU count |
| Commit | miner | Commit.json | Merkle root of the leaf hashes plus hashes of the final state and delta |
| Receipt | K_coord | Receipt.json | Proof of the drand round in which a commit arrived |
| DeltaManifest | miner | DeltaManifest.json | Where the delta lives, its size and chunk hashes |
| AuditChallenge | K_coord | AuditChallenge.json | Which miner, which beacon round, full replay or segments |
| StateServe | miner | StateServe.json | Optimizer state at a segment start, with a Merkle proof |
| ReplayVerdict | an auditor | ReplayVerdict.json | MATCH, MISMATCH, WITHHELD, BAD_PROOF or ASSIGNMENT_VIOLATION |
| Dispute | miner | Dispute.json | Accept or contest a verdict |
| Bisect | miner or auditor | Bisect.json | One level of N-ary bisection |
| Resolution | referee auditor | Resolution.json | Single operator that decides a dispute |
| Forfeit | K_coord | Forfeit.json | Burn and blacklist record |
| Rollback | aggregator | Rollback.json | Excluded miners and recomputed state hashes |
| Finalize | aggregator | Finalize.json | Makes round w final and fixes the paid set |
LeafPreimage (LeafPreimage.json) is not sent as a message. It is the pre-image of one Merkle leaf and is revealed on audit. A fully populated manifest is in example-run-manifest.json.
All bodies reject unknown fields. Integers are StrictInt in [0, 2^53-1] (JSON true is not 1). Floats never travel as JSON numbers: they are 4-byte little-endian f32 rendered as 8 hex characters, so canonicalization never sees a float.
Every message is wrapped as
{v: "ht/1", type, run_id, body, signer, exp_drand, sig}
and nothing else is allowed in it. run_id is 64 lowercase hex characters, exp_drand a positive integer drand round, sig 64 bytes as 128 hex characters.
- Canonical JSON. Bodies are serialized with RFC 8785 JCS (
protocol/jcs.py). Lone surrogates, NaN and Infinity are refused. The intake additionally requires that the received body equals the canonical dump of the validated model, so a body with defaults filled in or fields reordered is rejected. - Signing message.
"hypertrain/1|" + type + "|" + sha256_hex(JCS(body)) + "|" + exp_drand + "|" + run_id. Appendingrun_idis a deliberate addition to the advisory preimage: a body signed for one run can never verify in another. - Scheme. sr25519 over the signing message. Signers are SS58 hotkeys; K_coord and K_owner are also SS58 keys named in the manifest (
coord_pubkey) or configured on the container. - Expiry. An envelope is refused once
now_round > exp_drand. Container-issued records (Receipt, AuditChallenge, Forfeit, round-0 RoundOpen) useexp_drand = 2^53-1. - Signer binding. For Commit, Accept, DeltaManifest, StateServe and Dispute the body
hotkeymust equal the envelope signer. Other types are checked against an allow-list per type (auditors for ReplayVerdict and Resolution, K_coord for container records). - Replay protection. The key is
(run_id, w, type, signer, subject), wheresubjectis the first present ofhotkey,target,commit_hash. A Dispute is keyed by itsverdict_hashinstead, so a miner gets one decision per verdict. The challenge container persists the seen set in SQLite, so replays stay refused after a restart. - Order of checks in
Intake.accept: shape, run id, signature, expiry, signer allow-list, model validation, canonical form, signer binding, replay.
Failures map to HTTP codes in challenge-routes.md: malformed 400 or 422, bad signature 400, not allowed 403, replay or wrong state 409.
- Tensor hash TH(state). For each tensor,
sha256(lp(name) | lp(dtype) | lp(shape) | lp(raw little-endian bytes))wherelp(x)is a u64-LE length prefix andshapeisu32 ndim | u64 dims. TH is the sha256 of the concatenated entry digests sorted by the UTF-8 bytes of the name. Duplicate names and negative dimensions raise. - Merkle tree. Leaf =
sha256(0x00 | x), node =sha256(0x01 | left | right), an odd last node is promoted unchanged. - Leaf scheme
ht-leaf-v1. One leaf perJinner steps plus the initial state, son_leaves = H/J + 1andJmust divideH. A leaf digest is
sha256( "ht-leaf-v1" | run_id(32B) | w u64le | t u64le | stage_root
| batch_ids_sha256(32B) | rng_ctr u64le | loss_f32(4B) | norm_f32(4B) )
with stage_root the Merkle root over theta | m | v hashes per pipeline stage. Commit.leaves_root is the Merkle root of the leaf digests and metrics_root commits the per-leaf loss and norm.
- Assignment.
assignment_hash = sha256("ht-assignment-v1" | run_id | u64be(w) | u64be(slot) | u64le(sample)*). The sample slices are a pure function ofrun_id,w,drand_sig[d_assign]and the manifest, so the auditor recomputes them and raises ASSIGNMENT_VIOLATION if the miner trained on other data. - Audit selection.
sha256("ht-audit" | run_id | u64be(w) | drand_sig[d_audit] | hotkey) < q_i * 2^256, exact on the f32 value ofq_i. - Segment selection (carry policy). Fisher-Yates over leaf windows seeded with
sha256("ht-segments|" + run_id|w|beacon_sig_sha256|target),krandom windows, the final window always, and theQ_topwindows with the largest committednorm_f32.
All times are drand rounds D from the manifest beacon (quicknet, period 3 s). drand_round_at(t) = (t - genesis)//period + 1.
RoundOpen carries d_open < d_assign < d_commit < d_audit < d_final and d_commit < d_upload <= d_final; any other order fails validation. The manifest timeouts are offsets from these anchors (example manifest values):
| Timeout | Rounds | Meaning |
|---|---|---|
assign_after_open |
20{ev:docs/schemas/example-run-manifest.json#manifest.verify.T.assign_after_open} | d_assign - d_open |
upload_after_commit |
300{ev:docs/schemas/example-run-manifest.json#manifest.verify.T.upload_after_commit} | d_upload - d_commit |
audit_after_commit |
40{ev:docs/schemas/example-run-manifest.json#manifest.verify.T.audit_after_commit} | d_audit - d_commit, validated >= 40 as clock-skew margin |
serve_deadline |
900{ev:docs/schemas/example-run-manifest.json#manifest.verify.T.serve_deadline} | miner must publish StateServe |
dispute_per_level |
600{ev:docs/schemas/example-run-manifest.json#manifest.verify.T.dispute_per_level} | per bisection level |
Round states are derived from D and stored facts, never from a mutable flag:
OPEN -> ASSIGNED -> TRAINING -> COMMIT_CLOSED -> UPLOAD_CLOSED -> APPLIED -> AUDIT
AUDIT <-> DISPUTE (while any dispute is open)
AUDIT -> FINAL -> VESTING -> RELEASED
| State | Condition |
|---|---|
| OPEN | D < d_assign |
| ASSIGNED | D == d_assign |
| TRAINING | d_assign < D < d_commit |
| COMMIT_CLOSED | D >= d_commit |
| UPLOAD_CLOSED | D >= d_upload, aggregate not yet posted |
| APPLIED | round w+1 opened (optimistic apply), until its d_assign |
| AUDIT / DISPUTE | after that; DISPUTE while a dispute row is open |
| FINAL | Finalize stored |
| VESTING | ledger round is past the finalize round |
| RELEASED | ledger round >= final_round + vest_rounds |
A commit received at D >= d_audit gets status EXCLUDED: no ledger commit, no entitlement. The audit selection runs once drand[d_audit] is known, which is after every accepted commit, so the seed cannot be predicted by the committer.
Miner status: ASSIGNED, ACCEPTED, COMMITTED, UPLOADED, EXCLUDED, MATCH, MISMATCH, WITHHELD, BAD_PROOF, ASSIGNMENT_VIOLATION.
Run lifecycle. The owner posts a signed RunManifest (created), the operator sets the round timing with PUT .../config, then unpauses. The challenge container requires coord_pubkey to be its own K_coord and the ledger parameters to equal the manifest (epochs_per_round, E_vest_rounds, forgive_per_epoch). One run per state directory.
Dispute. Dispute{accept|contest} on a verdict, then Bisect envelopes (level step, layer, op; N between 2 and 1024; N+1 hashes; interval with a < b) until a Resolution names a single operator and a loser. A Resolution may not come from the disputed auditor. If the loser is the auditor the miner is restored to MATCH, and if the round was already finalized the ledger vindicate event pays the forgone slice. If the loser is the miner the ledger fault event burns the outstanding escrow.
Rollback. Only before Finalize, and only for committed miners of round w. The aggregator recomputes agg_w, step_w, agg_w1, step_w1 from stored artifacts, reports the old and new theta hashes of round w+2, and opens round w+2 from the corrected state. After Finalize there is no rollback: later fraud is handled with money only (clawback, see the mechanism document).
| Verdict | Forfeit cause | Effect at finalize |
|---|---|---|
| MATCH | none | paid |
| MISMATCH, BAD_PROOF | MISMATCH | FAULT, blacklist |
| WITHHELD | WITHHELD | FAULT, blacklist |
| ASSIGNMENT_VIOLATION | ASSIGNMENT_VIOLATION | FAULT, blacklist |
| lost dispute | DISPUTE_LOST | FAULT, blacklist |
| MISMATCH reproduced by the miner's own rerun | TRANSIENT | round reward burns, escrow kept, forgiven forgive_per_epoch times per chain epoch |
| no upload | NO_UPLOAD | slice burns, escrow kept, payable on vindication |
The ledger is a hash-chained journal of integer events: commit, verdict, finalize, clawback, vindicate, fault, plus the weights answers. State is a pure replay of the journal, and on open every stored answer is recomputed byte for byte. A torn tail is quarantined, a flipped byte raises ChainBreak. Event times must be non-decreasing and strictly after the last answered epoch_at.
get_weights(epoch, epoch_at) follows Cortex algorithm 3: full_share_mass = 10^6 per epoch, unpaid mass burns, the first answer per epoch is persisted and returned unchanged, at most 65,536 hotkeys are listed (largest kept, ties by hotkey, rest carried). While a round is in AUDIT the endpoint answers 200 with the vested mass only, never 503.
An auditor and a miner must produce identical bits on any allowed host. A manifest fixes reference_spec: image_digest, spec_doc_sha256, driver_allowlist, sm_count, env and the island layout. The checklist:
- Image pinned by digest. torch, CUDA, cuBLAS, cuDNN and NCCL come from that image.
CUDA_DISABLE_PTX_JIT=1, so only precompiled SASS runs. - Environment before
import torch.CUBLAS_WORKSPACE_CONFIG=:4096:8(the only accepted value).setup_determinismraises if torch was imported first without it. - Torch flags.
use_deterministic_algorithms(True)withoutwarn_only,cudnn.benchmark=False,cudnn.deterministic=True, TF32 off for matmul and cuDNN, reduced-precision reductions off for bf16 and fp16,float32_matmul_precision("highest"). - CPU threads.
torch.set_num_threads(cpu_threads)from the manifest; the trainer refuses to start if the live value differs. - Hardware gate.
sm_countmust equal the manifest (170 for RTX 5090) and the device name must match. The driver is set by the host and cannot live in the image: the miner reports it in Accept and the auditor replays on an allow-listed driver. - Shapes. Fixed micro-batch and sequence length, pre-tokenized u32 shards, no dynamic padding.
7a. Aggregator norms.
ht-norm-v1(default; every existing run): the exactly rounded sum of float64 products (math.fsumsemantics), computed vectorised by exact integer binning per float64 exponent.ht-norm-v2(opt-in, new runs): per tensor, float32 values are squared (or multiplied) in float64 (exact), zero-padded to a power of two, then reduced by the fixed pairwise treex = x[0::2] + x[1::2]until one value remains; only elementwise IEEE adds are used, nevernp.sum/np.add.reduce, whose blocking is build-dependent. Per-tensor results are combined withmath.fsumin UTF-8 tensor-name order. Implemented inhypertrain.aggregator.core(NORMS). - Kernels. Deterministic attention backward, MoE router ties broken by the lowest index, fixed-order reductions. Cross-rank sums inside an island are fixed-order
all_gatherplus local sum (forbid_reductionsguards against NCCLall_reduce). - Master state. fp32 master weights and optimizer state;
compute_dtypeisbf16orfp32. - Evidence. Phase A on three rented hosts is summarized in mechanism.md (section "Cross-host bitwise result"). The 8-GPU island result (Phase B) is in the same document, subsection "Cross-host bitwise result on 8-GPU islands".