Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions docs/scoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ then by recomputed prequential **bits-per-byte**. The challenge owns every numbe
> wins remain **provisional only**. Challenge-owned **`prism_train_series.v1`** is visibility + residual
> densify for sample-eff/stability only — **never sole primary** over held-out/bpb. When an Official
> grade pin **requires** the series and it is missing/corrupt, Official grade **fail-closes** (not
> silent PASS); miner dashboards remain non-authoritative. Provider trust + **IMAGE_PIN** govern
> worker integrity; **REAL-PROVIDER TEE** is **retired** for Prism product (historical tables may
> still say BLOCKED; never a production scoring gate).
> silent PASS); miner dashboards remain non-authoritative. Worker-plane integrity uses
> **constation** (`constation_ok` over the six-mechanism bundle). **REAL-PROVIDER TEE** is
> **retired** for Prism product (historical tables may still say BLOCKED; never a production
> scoring gate). Under P1, a missing or failed constation bundle means **no score row at all**,
> so attestation is **decisive for score presence** (not a soft label orthogonal to ranking).
```mermaid
flowchart LR
Loss[Single-pass online loss stream] --> Bpb[Prequential bits-per-byte]
Expand Down Expand Up @@ -155,10 +157,28 @@ Every number above is recomputed by the challenge from the challenge-authored
raw-loss term and the v1-NAS architecture/training ownership pools are retired from the score.

Miner self-reports remain non-authoritative on both the emission path and Official Comparison mode
(including scorecard v1.1, Complete View, and train series). Provider-trust / IMAGE_PIN / LAB-GPU
labels are orthogonal to ranking; **REAL-PROVIDER TEE** is a retired product goal (historical
non-claims only): see [Official Comparison](official-comparison.md) §17 train series telemetry,
scorecard honesty, and [Security](security.md).
(including scorecard v1.1, Complete View, and train series).

## Constation and score presence (P1 fail-closed)

Worker-plane Lium runs are admitted to the emission path only when Prism accepts a full
constation bundle and `constation_ok` is true. That predicate is the sole route to
`effective_tier == 1` (ceiling **1**; no TEE). Self-reported image digests and historical
"IMAGE_PIN match" labels do **not** elevate tier alone and do **not** create a score row
when the bundle is missing or fails.

| Outcome | Score row? | Ranking impact |
| --- | --- | --- |
| Valid bundle, `constation_ok` | Yes (then held-out/bpb ranking applies) | Eligible for emission crown |
| Missing bundle | **No** (`miner_fault:missing_constation_bundle`) | Not ranked |
| Failed mechanism(s) | **No** (`miner_fault:<code>`) | Not ranked |
| Infra outage after retries | **No** (`infra_fault:*`) unless audited break-glass | Break-glass admits at tier 0 only |

So constation is **not orthogonal to ranking**: without a score row there is nothing to rank.
Among admitted rows, ranking remains held-out primary / bpb secondary as above. Trust model
detail and the six mechanisms with honest limitations: [Security](security.md) and
[prism-recipe security](../../prism-recipe/docs/security.md). Official Comparison notes:
[Official Comparison](official-comparison.md) §17 train series telemetry and scorecard honesty.

## Reference Studies

Expand Down
82 changes: 64 additions & 18 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,63 @@ or legacy reduced bodies fail closed with a 422 before scoring or persistence. P
plausibility gates run before finalization; duplicate deliveries are idempotent and conflicts refuse
mutation of sealed scores.

## Provider Trust And IMAGE_PIN

Prism does **not** ship a TEE-attestation verifier and does **not** require TEE evidence to finalize
production scores. Integrity for miner-funded GPU work rests on:

- **PROVIDER_TRUST** — operators trust **Lium/Targon** as compute providers (no Prism crypto TEE path).
- **IMAGE_PIN** — `worker_plane.pinned_image_digest` match grants audit effective tier **1** (maximum);
pin mismatch yields an honest non-elevated downgrade (not silent ignore).
- **DEPLOY SMOKE** — paid provider lifecycle proofs are reachability/infra only; always terminate pods.
- **LAB-GPU** — remote CUDA lab scores under Official Comparison; scientific only.
- Ordinary ExecutionProof envelope + worker signature checks on the worker-plane path.

**REAL-PROVIDER TEE** (cryptographic provider attestation PASS as a Prism product goal) is
**retired**. Historical lab tables and reports may still show `real_provider_tee=BLOCKED` as honesty
history; do not implement or document a TEE production scoring gate as the live path. Opaque
non-empty `tdx_quote_b64` / `gpu_eat_jwt` fields never imply tier 2 by presence alone (max effective
tier is 1 via image pin).
## Worker-plane integrity: constation (tamper-evidence only)

### Threat model (binding)

- The miner **rents and controls** the Lium pod, including **root**.
- Integrity checks are **tamper-evidence**, not tamper-prevention. A determined root miner can
defeat all six mechanisms below.
- Prism does **not** ship a TEE verifier and does **not** require TEE evidence to finalize scores.
- `effective_tier` **ceiling is 1**. Claimed tier ≥ 2 always collapses to 0. Opaque
`tdx_quote_b64` / `gpu_eat_jwt` fields never imply tier 2 by presence alone.
- Lium TDX (or similar host quotes), if present in the wild, attest **executor infrastructure**,
not the renter workload inside the pod.
- The Lium API image/template digest is **declared configuration**, not a runtime measurement of
the container rootfs.

### Sole elevation and score gate

| Predicate | Role |
| --- | --- |
| `constation_ok(bundle)` | **Only** path that can grant `effective_tier == 1` |
| Self-reported `PRISM_IMAGE_DIGEST` / pin string match | Telemetry only; **never** elevates alone |
| Missing or failed constation bundle | **No score row written** (P1 fail-closed) |
| Operator break-glass | May admit **infra_fault** runs only; never `miner_fault`; score at tier 0 |

Historical label **IMAGE_PIN** meant "digest pin matched." Under P1 that label is **not** a soft
orthogonal badge: without a valid constation bundle there is no score to rank. See
[scoring.md](scoring.md).

`attestation_mode` on proofs is `miner_rent_image_pin_evidence_v1` (evidence under miner-rent pods).
Forbidden modes include any TEE-implying string (`tee`, `tdx`, `sev`, …).

### Six mechanisms (each with honest limitation)

1. **Nonce-bound attestation** — BASE per-run nonce; sidecar signs bound fields; defeats replay.
*Limit:* live root with the build secret can still answer fresh challenges.
2. **Same-account corroboration** — Lium API declared digest must agree with sidecar report.
*Limit:* **never independent**; miner supplies the API key and owns the pod. Agreement alone
never elevates if another check fails.
3. **Continuous constation** — start / end / randomized polls; gap or drift fails the bundle.
*Limit:* shrinks swap windows; does not seal the kernel against a twin process.
4. **BASE-built digest allowlist** — only BASE-produced digests for a registered commit score.
*Limit:* proves build provenance, not that the pod still runs that digest after deploy.
5. **In-image self-measurement** — sidecar hashes sealed harness / rules / data-window at runtime.
*Limit:* software on a miner-controlled host; root can patch the measurer.
6. **Build-time per-build secret** — unique BASE-injected secret required in signed responses.
*Limit:* root on the pod can extract `/run/prism/attestation_hmac_key`.

Full recipe-side narrative: [prism-recipe security](../../prism-recipe/docs/security.md).

### Provider trust (operational, not crypto)

Operators still choose which GPU providers to enable (e.g. Lium). That is operational trust in
billing and reachability, **not** a cryptographic claim that the renter workload is honest.
Deploy smoke checks are reachability/infra only; always terminate pods.

**REAL-PROVIDER TEE** as a Prism product goal is **retired**. Historical lab tables may still show
`real_provider_tee=BLOCKED` as honesty history; do not implement a TEE production scoring gate.

## Locked Data, No Network

Expand All @@ -106,6 +146,10 @@ Host-side static instantiation and held-out scoring run in bounded child process
containers; evaluation is the long-lived challenge runtime (or external worker-plane GPUs on trusted
providers when enabled).

On the **miner-rent Lium** path, isolation is weaker by design: the miner is root on their pod.
Constation is the evidence layer for that path, not a substitute for container isolation on BASE-owned
evaluators.

## ZIP Hardening

ZIP extraction rejects symlinks, path traversal, unsafe paths, unsupported file types, and excessive
Expand All @@ -132,5 +176,7 @@ their own wallets. The challenge and master never write weights on-chain.
- Keep public submissions disabled when PRISM is deployed only behind BASE.
- Keep the eval container on `network=none` and the rootfs read-only except `artifacts_dir`.
- Do **not** configure LLM gateway URL/token fields; those surfaces are gone and residual knobs fail closed.
- Prefer `worker_plane.pinned_image_digest` for IMAGE_PIN tier-1; do not enable any removed TEE production scoring path.
- Require a full constation bundle for Lium worker-plane scores; do not treat pin string match as tier 1.
- Do not enable any removed TEE production scoring path.
- Monitor rejected, failed, and completed submissions separately (legacy held is not a live path).
- Attribute rejects with `miner_fault:*` vs `infra_fault:*`; break-glass only for infra with operator audit.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies = [
"aiosqlite>=0.20",
"fastapi>=0.115",
"httpx>=0.27",
"base @ https://github.com/BaseIntelligence/base/releases/download/v3.1.2/base-3.1.2-py3-none-any.whl#sha256=3a61c2d3a343ed6de55e80215486e3de0c9639276443d08f2ed316bc807f2ff0",
"base @ https://github.com/BaseIntelligence/base/releases/download/v3.2.0/base-3.2.0-py3-none-any.whl#sha256=0fdd79b8eacda77132d01173df2057730350f3e82fb5a8a4478e7a53e61852c3",
"pydantic>=2.8",
"pydantic-settings>=2.4",
"PyYAML>=6.0",
Expand Down
126 changes: 126 additions & 0 deletions src/prism_challenge/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,113 @@
from .weights import get_weights


def _constation_ingest_kwargs(
app_settings: PrismSettings,
result_payload: object,
*,
app: FastAPI | None = None,
) -> dict:
"""Resolve constation kwargs for ingest: prod wire OR insecure test seam.

Production: deserialize ``result.constation_bundle`` and attach checkers.
Preference:
1) BASE HTTP when ``constation_base_url`` + token (durable master SoT)
2) in-process app.state services (same issuer as local challenge routes)
Missing bundle returns {} so ingest fail-closes (P1). Never auto-injects
synthetic bundles in prod.
"""
if getattr(app_settings, "allow_insecure_signatures", False):
return _test_constation_kwargs(app_settings, result_payload)

if not isinstance(result_payload, dict):
return {}
raw_bundle = result_payload.get("constation_bundle")
if raw_bundle is None:
return {}

from .constation import constation_bundle_from_dict

try:
bundle = constation_bundle_from_dict(raw_bundle)
except (TypeError, ValueError):
# Malformed embedded bundle — leave to gate as missing/invalid.
return {}

base_url = getattr(app_settings, "constation_base_url", None)
token = getattr(app_settings, "constation_internal_token", None) or ""
if base_url and token:
from .constation_checkers import BaseHttpConstationClient

client = BaseHttpConstationClient(base_url=str(base_url), token=str(token))
return {
"constation_bundle": bundle,
"check_allowlist": client.check_allowlist,
"check_nonce": client.check_nonce,
"verify_constation_signature": client.verify_signature,
}

if app is not None:
from .attestation_routes import make_inprocess_checkers

try:
checkers = make_inprocess_checkers(app)
return {
"constation_bundle": bundle,
"check_allowlist": checkers["check_allowlist"],
"check_nonce": checkers["check_nonce"],
"verify_constation_signature": checkers["verify_constation_signature"],
}
except Exception:
pass

# Bundle present but checkers not configured: still pass bundle so
# constation_ok fails without elevating (checkers required → fail closed).
return {"constation_bundle": bundle}


def _test_constation_kwargs(app_settings: PrismSettings, result_payload: object) -> dict:
"""Supply a valid constation bundle only under allow_insecure_signatures (unit tests).

Production (allow_insecure_signatures=False) never auto-injects — missing bundle
fail-closes with no score (P1 / todo 22). Result payloads may still carry an
explicit constation block later; this helper is the test seam only.
"""
if not getattr(app_settings, "allow_insecure_signatures", False):
return {}
# If the payload already embeds constation markers, do not override.
if isinstance(result_payload, dict) and result_payload.get("constation_bundle") is not None:
return {}
from .constation import CheckOutcome, ConstationBundle

def _ok(**_k: object) -> CheckOutcome:
return CheckOutcome(ok=True, reason="ok")

man = {"route-test-harness.py": "a" * 64}
digest = "sha256:" + ("11" * 32)
bundle = ConstationBundle(
commit_sha="a" * 40,
tree_sha="b" * 40,
variant="cuda",
digest=digest,
work_unit_id="route-wu",
miner_hotkey="route-hk",
pod_id="route-pod",
nonce="route-nonce",
signed_attestation={"route": True},
expected_sealed_manifest_hashes=dict(man),
reported_sealed_manifest_hashes=dict(man),
lium_declared_digest=digest,
constation_gap_budget_seconds=30.0,
constation_observed_max_gap_seconds=1.0,
)
return {
"constation_bundle": bundle,
"check_allowlist": _ok,
"check_nonce": _ok,
"verify_constation_signature": lambda _s: _ok(),
}


def create_app(
app_settings: PrismSettings | None = None,
*,
Expand Down Expand Up @@ -147,6 +254,15 @@ async def _run_raw_weight_push(app: FastAPI) -> None:
app.state.checkpoint_publisher = publisher
app.state.checkpoint_intake = checkpoint_intake

# Attestation constation hosts (public challenge via routes; internal check/*).
from .attestation_routes import (
build_attestation_internal_router,
ensure_default_constation_services,
)

ensure_default_constation_services(app)
app.include_router(build_attestation_internal_router())

@app.post("/internal/v1/worker/process-next", dependencies=[Depends(authenticate_internal)])
async def process_next() -> dict[str, str | None]:
return {"submission_id": await worker.process_next()}
Expand Down Expand Up @@ -320,6 +436,7 @@ async def work_unit_result(request: Request) -> dict[str, object]:
result=result_payload,
pinned_image_digest=app_settings.worker_plane.pinned_image_digest,
audit_sampler=sampler,
**_constation_ingest_kwargs(app_settings, result_payload, app=request.app),
)
except ResultIngestionError as exc:
# A transient finalization failure is retryable -> 503 so the forwarder retries; the
Expand All @@ -343,6 +460,15 @@ async def work_unit_result(request: Request) -> dict[str, object]:
status.HTTP_409_CONFLICT,
{"code": outcome.reason, "detail": "conflicting result for finalized unit"},
)
if outcome.status == "rejected":
# P1 fail-closed: no score written; surface the miner/infra fault code.
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
{
"code": outcome.reason or "constation_rejected",
"detail": "constation gate refused score",
},
)
return outcome.to_response()

@app.post(
Expand Down
Loading
Loading