R2 is the primary host for the u16 shard sets (DESIGN A14). Egress is free, so miners can Range-read as much as they need. Code: hypertrain.storage (client, publisher, doctor); CLI: hypertrain.datasets publish.
Dashboard: R2 > Create bucket (e.g. hypertrain-data; location hint near the aggregator). Default storage class is fine; Infrequent Access adds a retrieval fee and suits only old shard sets. Do not add a lifecycle rule that deletes shard keys: they are content-addressed and referenced by signed DatasetSpecs. A rule that aborts incomplete multipart uploads is harmless. Temp keys of doctor live under _doctor/.
R2 > Manage API tokens > Create: permission Object Read & Write, scoped to this bucket only. Put the Access Key ID and Secret in an untracked .env (copy .env.example; never commit it) or the process environment. Variables: R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_BASE, optional R2_ENDPOINT (MinIO or any S3 endpoint). Environment variables override --env-file values.
Bucket > Settings > Custom Domains (recommended; caching and WAF) or enable the r2.dev URL (rate limited, testing only). Set R2_PUBLIC_BASE to that URL, no trailing slash. Check Range support:
curl -s -o /dev/null -D- -H 'Range: bytes=0-15' "$R2_PUBLIC_BASE/manifest.json" | head -3 # HTTP/2 206CORS (Bucket > Settings > CORS policy), needed only for browser readers:
[{"AllowedOrigins": ["*"], "AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["Range"], "ExposeHeaders": ["Content-Range", "Content-Length", "ETag"],
"MaxAgeSeconds": 86400}]uv run --frozen python -m hypertrain.storage doctor [--env-file .env] [--cors-origin https://app.example]It does PUT (If-None-Match), HEAD (size and sha256 metadata), signed Range GET, public Range GET (must be 206, plus CORS if asked) and DELETE on a temp key. Exit 0 = all ok; 2 = missing variables (named); 1 = a check failed.
uv run --frozen python -m hypertrain.datasets build --mix od-a-v1 --out data/od --cache cache
uv run --frozen python -m hypertrain.datasets verify data/od
uv run --frozen python -m hypertrain.datasets publish data/od --to r2 --env-file .env [--prefix sets/od-a-v1]Order: shards at shard_uri_template keys (default {shard_sha256}), then build_record.json, then manifest.json last, so a reader never sees a manifest with missing shards. Each PUT sends x-amz-content-sha256 and stores x-amz-meta-sha256; shard keys use If-None-Match: * (immutable). Re-running HEADs each key and skips matching objects, so an interrupted run resumes. At the end --verify-units N (default 8) Range-reads random units through R2_PUBLIC_BASE and checks the unit sha256s; a failure is non-zero. Shards are at most 64 MiB, so single PUT is enough (no multipart).
uv run --frozen python -m hypertrain.datasets publish-labels <label_cache_dir> --prefix labels/<teacher>-<mix> --env-file .envEvery file is uploaded under the prefix at its relative path (existing identical ones are skipped), then labels-index.json (path, size, sha256 per file) last. Consumers read the index first and verify each file against it.
The signed DatasetSpec carries shard_uri_template, shard_sha256_root and unit_sha256_root. The base URL (R2_PUBLIC_BASE plus the prefix) is the operator-supplied entry in the base list handed to fetch.prefetch(..., bases=[...]), with the HF mirror second. The base is not signed: miners check bytes against the signed unit hashes, so a wrong or hostile base can only fail, not poison.
Checkpoints, theta, miner uploads (the challenge object store, hypertrain.data.store, has its own S3Store), the ledger and the holdout (private until reveal) are not part of the R2 dataset flow.