Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4959f07
feat(proof): eval executor pin ceilings, topic tighten, and EvalExecu…
cursoragent Sep 8, 2026
6bcd2eb
feat(lium): exact_gpu_count spec aborts any rent that is not the pinn…
cursoragent Sep 8, 2026
f3eefb8
feat(harvest-pod): per-run deadline caps the pod timeout and classifi…
cursoragent Sep 8, 2026
5921d7b
feat(proof-eval): executor offer gates live readiness and the harvest…
cursoragent Sep 8, 2026
2ed5d8d
feat(proof-harvest): rent the executor template at exactly 1x and hol…
cursoragent Sep 8, 2026
9878827
feat(proof-http): public executor route, admin rotate, and executor-g…
cursoragent Sep 8, 2026
f55b5a3
feat(proof-challenge): load PROOF_EVAL_EXECUTOR_OFFER_FILE and wire t…
cursoragent Sep 8, 2026
3ceb90e
feat(xtask): proof-executor-offer ceremony builds a pin-validated 1x …
cursoragent Sep 8, 2026
dc93dd7
docs(proof): document the EvalExecutorOffer contract, admin route, an…
cursoragent Sep 8, 2026
35157ca
chore(proof): clippy fixes and boot_executor helper
cursoragent Sep 8, 2026
f7f1004
docs(proof): state executor isolation invariants; carry topic scope o…
cursoragent Sep 8, 2026
2389281
fix(harvest-pod): resolved deadline is the pod timeout; deadline prov…
cursoragent Sep 8, 2026
be2c39f
fix(xtask): proof-executor-offer refuses any tracked or trackable wor…
cursoragent Sep 8, 2026
d7ae7a0
fix(proof-executor): bind executor templates to the pinned digest; re…
cursoragent Sep 8, 2026
f02cfc9
fix(proof): refuse config-changing overrides on commitment-pinned top…
cursoragent Sep 8, 2026
a7f3976
docs(proof): executor binding, commitment, and deadline rules after r…
cursoragent Sep 8, 2026
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
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,13 @@ When verifying a challenge (local-e2e, staging, or focused tests), **simulate a
2. Edge / failure probes: bad harness, sanitize reject, quota, wrong routes/auth.
3. **Bounty — pair + report:** `ctx bounty pair --hotkey <ss58> --account-id <id> --accept-terms`, then `POST /v1/pair` (terms + signature) and `POST /v1/reports`. Operator bearer `POST /v1/admin/adjudicate` (`valid` / `already_fixed_not_prod` / `invalid_malicious` / `duplicate`). Scoring **reads** CortexLM/backend public JSON (`BOUNTY_BACKEND_PUBLIC_URL`); do not serve `/v1/public/*` from this repo.
4. **Bounty — fail-closed scorer:** the CortexLM/backend public feed is the only scorer. With no readable `BOUNTY_BACKEND_PUBLIC_URL`, `POST /v1/reports` must answer **503** and the emitter must pay **nobody** — it still covers `E` with `NoScore(ChallengeInternal)`, because a paid challenge with no leaves 409s the seal for every challenge. `BOUNTY_FORCE_SIM` is retired — do not reintroduce an offline bounty scorer. See [`docs/BOUNTY.md`](docs/BOUNTY.md).
5. **Proof — submit:** `POST /v1/submissions` with a `topic_id`. Missing/unknown/not-open → **400** (no row). Empty `eval_image_digest`, missing/closed/misconfigured RLM judge `InferenceOffer`, missing judge API key, spoofed topic origin, zero open topics, or an unsealed baseline → **503**. Miners submit claim + code + FLOPs + artifact; they do not bind the judge offer. Contamination / empty manifest persist **rejected** without rent. `GET /v1/proof/topics` must never leak holdout records.
6. Leaf emission → `POST /v1/weights/raw` → seal → `GET /v1/weights/latest` with **`sealed: true`** (burn fallback alone is not a real seal).
5. **Proof — submit:** `POST /v1/submissions` with a `topic_id`. Missing/unknown/not-open → **400** (no row). Empty `eval_image_digest`, missing/closed/misconfigured RLM judge `InferenceOffer`, missing judge API key, spoofed topic origin, missing/closed/non-`1x` `EvalExecutorOffer` (Lium path), zero open topics, or an unsealed baseline → **503**. Miners submit claim + code + FLOPs + artifact; they do not bind the judge offer or the executor offer. Contamination / empty manifest persist **rejected** without rent. `GET /v1/proof/topics` must never leak holdout records.
6. **Proof — executor:** `GET /v1/proof/executor` is always 200 (`ready` + `reason`); `POST /v1/admin/proof/executor` (operator bearer) rotates or closes the live `1x` offer and 400s anything the pin refuses. Harvest rents the offer's `lium_template_id` at exactly `1x` (any other `rent_gpu_count` aborts before the rent) under `max_proof_deadline_s`; a run cut at the deadline is **503 + `stdout_tail`**. `PROOF_HARVEST_*` env only hot-swaps under the pin ceilings. Never a live Lium rent in CI.
7. Leaf emission → `POST /v1/weights/raw` → seal → `GET /v1/weights/latest` with **`sealed: true`** (burn fallback alone is not a real seal).

**Never host Sim in staging/prod** for live scoring. `PROOF_FORCE_SIM=1` is CI/local opt-in only (`deploy/scripts/assert-compose-matrix.sh` fails if a droplet overlay sets one). Live Proof rent requires a digest pin in `config/proof-pin.toml` plus miner BYOK (`LIUM_API_KEY` / `X-Lium-Api-Key`). Never log or commit that key. Do not invent `eval_image_digest`.

**Bounty product rules:** pay is precision x severity, an unpriced `valid` row is not creditable, and the triage-noise ratio stays off the visible score. **Proof product rules (do not weaken):** topics are operator-published signed documents, not a git catalog; a topic may tighten a floor never loosen it; a baseline must be sealed to open; each topic is `wta` (winner takes the topic mass) or `discovery` (pass floor + novelty); global miner score is the **sum** of per-topic masses, not a mean of binary lattices; empty open set / empty eval digest fails closed (`503`); `custom` unknown ids refuse at publish; `harness_success_rate` is listed and fail-closes until the real harness exists.
**Bounty product rules:** pay is precision x severity, an unpriced `valid` row is not creditable, and the triage-noise ratio stays off the visible score. **Proof product rules (do not weaken):** topics are operator-published signed documents, not a git catalog; a topic may tighten a floor never loosen it; a baseline must be sealed to open; each topic is `wta` (winner takes the topic mass) or `discovery` (pass floor + novelty); global miner score is the **sum** of per-topic masses, not a mean of binary lattices; empty open set / empty eval digest fails closed (`503`); `custom` unknown ids refuse at publish; `harness_success_rate` is listed and fail-closes until the real harness exists; the eval executor is exactly `1x` (pin `gpu_class`), a topic may only tighten `eval_executor.max_proof_deadline_s` / pin `require_offer_commitment`, and there is no per-topic `machine_id`.

Local smoke automates the weights seal step via `weights-smoke` inside `./deploy/scripts/local-e2e.sh --smoke` (see [`deploy/AGENTS.md`](deploy/AGENTS.md) and [`docs/runbooks/local-testnet-e2e.md`](docs/runbooks/local-testnet-e2e.md)).

Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 138 additions & 5 deletions bins/proof-challenge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ use challenge_keys::load_challenge_secret;
use clap::Parser;
use prism_lium::LiumClient;
use proof_challenge::{
hash_admin_token, parse_holdout_file, proof_router, AppState, BaselineMeasurement, EvalBackend,
InferenceOffer, LiveScorer, MemoryStore, ProofPin, TopicDocument, CHALLENGE_ID,
SCORING_VERSION,
executor_slot, hash_admin_token, parse_holdout_file, proof_router, AppState,
BaselineMeasurement, EvalBackend, EvalExecutorOffer, HarvestOverrides, InferenceOffer,
LiveScorer, MemoryStore, ProofPin, TopicDocument, CHALLENGE_ID, SCORING_VERSION,
};
use proof_eval::supported_custom;
use proof_harvest::{HarvestLimits, LiumProofHarvest};
Expand Down Expand Up @@ -59,8 +59,14 @@ struct Cli {
/// Operator holdout records (JSON array or map keyed by topic id). Never in git.
#[arg(long, env = "PROOF_HOLDOUT_FILE")]
holdout_file: Option<PathBuf>,
/// Seconds the eval image gets to score one artifact on the pod.
#[arg(long, env = "PROOF_EVAL_TIMEOUT_SECS", default_value_t = 5400)]
/// Fallback seconds the eval image gets when no executor deadline was
/// resolved. A resolved `max_proof_deadline_s` is the pod timeout and is
/// never clamped by this value; the default equals the pin ceiling.
#[arg(
long,
env = "PROOF_EVAL_TIMEOUT_SECS",
default_value_t = proof_task::MAX_PROOF_DEADLINE_S_CEILING
)]
eval_timeout_secs: u64,
/// Sealed baseline measurements (JSON map keyed by topic id).
#[arg(long, env = "PROOF_BASELINE_FILE")]
Expand All @@ -71,6 +77,11 @@ struct Cli {
/// Provider API key file. Never logged, never on `/v1/status`.
#[arg(long, env = "PROOF_INFERENCE_API_KEY_FILE")]
inference_api_key_file: Option<PathBuf>,
/// Live `1x` `EvalExecutorOffer` JSON (Lium template + proof deadline).
/// Operator state; never a git pin. Rotated at runtime via
/// `POST /v1/admin/proof/executor`. Missing/closed/shape ≠ 1x → 503.
#[arg(long, env = "PROOF_EVAL_EXECUTOR_OFFER_FILE")]
eval_executor_offer_file: Option<PathBuf>,
/// Local measurement weights staged onto the eval pod (no HF bake).
#[arg(long, env = "PROOF_PROXY_MODEL_DIR")]
proxy_model_dir: Option<PathBuf>,
Expand Down Expand Up @@ -151,6 +162,7 @@ fn run(cli: &Cli) -> Result<(), String> {
),
_ => {}
}
let executor = boot_executor(&pin, backend, cli.eval_executor_offer_file.as_deref());

let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
Expand Down Expand Up @@ -181,6 +193,7 @@ fn run(cli: &Cli) -> Result<(), String> {
backend,
live_scorer,
offer,
executor: executor_slot(executor),
judge_api_key,
admin_hashes: Arc::new(load_admin_hashes(cli.admin_tokens_file.as_deref())),
epoch: 0,
Expand Down Expand Up @@ -359,6 +372,54 @@ fn load_offer(pin: &ProofPin, path: Option<&Path>) -> Result<InferenceOffer, Str
Ok(offer)
}

/// Load the live executor offer and log the harvest hot-swap state. Neither
/// is a boot error: the Lium path answers 503 until an open `1x` offer is on
/// the host (file or `POST /v1/admin/proof/executor`).
fn boot_executor(
pin: &ProofPin,
backend: EvalBackend,
path: Option<&Path>,
) -> Option<EvalExecutorOffer> {
match HarvestOverrides::from_env() {
Ok(o) if !o.is_empty() => {
tracing::info!(?o, "PROOF_HARVEST_* override set; pin ceilings still bind");
}
Ok(_) => {}
Err(e) => tracing::warn!("{e}; every live harvest will refuse until it is fixed"),
}
match load_executor(pin, path) {
Ok(x) => {
tracing::info!(
offer_id = %x.offer_id,
lium_template_id = %x.lium_template_id,
machine_shape = %x.machine_shape,
max_proof_deadline_s = x.max_proof_deadline_s,
status = ?x.status,
"eval executor offer loaded"
);
Some(x)
}
Err(e) => {
if backend == EvalBackend::Lium {
tracing::warn!(
"eval executor offer unavailable ({e}); live submits will 503 until \
PROOF_EVAL_EXECUTOR_OFFER_FILE holds an open 1x offer or one is posted \
to /v1/admin/proof/executor"
);
}
None
}
}
}

fn load_executor(pin: &ProofPin, path: Option<&Path>) -> Result<EvalExecutorOffer, String> {
let p = path.ok_or("PROOF_EVAL_EXECUTOR_OFFER_FILE not set")?;
let body = std::fs::read_to_string(p).map_err(|e| format!("read {}: {e}", p.display()))?;
let offer = EvalExecutorOffer::from_json(&body).map_err(|e| e.to_string())?;
offer.validate(pin).map_err(|e| e.to_string())?;
Ok(offer)
}

fn load_admin_hashes(path: Option<&Path>) -> Vec<String> {
let Some(p) = path else {
return Vec::new();
Expand Down Expand Up @@ -519,4 +580,76 @@ mod tests {
}

static OFFER_ENV: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Compose sets `PROOF_EVAL_EXECUTOR_OFFER_FILE`. Missing / unparseable /
/// non-`1x` is `can_score=false` / submit 503 — never `exit 1`. A valid
/// `1x` offer on the committed pin's digest-scoped template loads.
#[test]
fn compose_executor_offer_env_parses_and_bad_files_are_not_boot_errors() {
let _guard = OFFER_ENV
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::env::set_var(
"PROOF_EVAL_EXECUTOR_OFFER_FILE",
"/run/base/proof/eval_executor_offer.json",
);
let cli = Cli::try_parse_from(["proof-challenge"])
.unwrap_or_else(|e| panic!("PROOF_EVAL_EXECUTOR_OFFER_FILE broke parsing: {e}"));
assert_eq!(
cli.eval_executor_offer_file.as_deref(),
Some(Path::new("/run/base/proof/eval_executor_offer.json"))
);
std::env::remove_var("PROOF_EVAL_EXECUTOR_OFFER_FILE");

let pin_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../config/proof-pin.toml");
let pin = load_pin(Some(&pin_path)).expect("committed pin");
let missing = load_executor(&pin, Some(Path::new("/nonexistent/executor.json")))
.expect_err("missing file is unavailable, not a panic");
assert!(missing.contains("read"), "{missing}");
assert!(load_executor(&pin, None)
.expect_err("unset")
.contains("PROOF_EVAL_EXECUTOR_OFFER_FILE"));

let dir = std::env::temp_dir().join(format!(
"proof-executor-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
std::fs::create_dir_all(&dir).expect("dir");
let hex = pin.eval_image_digest.trim_start_matches("sha256:");
let mut good = EvalExecutorOffer {
offer_id: "lium-1x-v0".into(),
lium_template_id: format!("proof-eval-{}", &hex[..12]),
machine_shape: "1x".into(),
max_proof_deadline_s: 7_200,
eval_image_digest: pin.eval_image_digest.clone(),
config_commitment: String::new(),
status: proof_challenge::OfferStatus::Open,
};
good.config_commitment = good.expected_commitment();
let good_path = dir.join("good.json");
std::fs::write(&good_path, serde_json::to_vec(&good).expect("json")).expect("write");
let loaded = load_executor(&pin, Some(&good_path)).expect("valid 1x offer loads");
assert_eq!(loaded, good);

let mut wide = good.clone();
wide.machine_shape = "8x".into();
wide.config_commitment = wide.expected_commitment();
let wide_path = dir.join("wide.json");
std::fs::write(&wide_path, serde_json::to_vec(&wide).expect("json")).expect("write");
let err = load_executor(&pin, Some(&wide_path)).expect_err("8x is refused");
assert!(err.contains("machine_shape"), "{err}");

let junk_path = dir.join("junk.json");
std::fs::write(
&junk_path,
b"{\"offer_id\":\"x\",\"lium_api_key\":\"nope\"}",
)
.expect("write");
let err = load_executor(&pin, Some(&junk_path)).expect_err("unknown key");
assert!(err.contains("lium_api_key"), "{err}");
let _ = std::fs::remove_dir_all(&dir);
}
}
21 changes: 21 additions & 0 deletions config/proof-pin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@
# (open/closed). Miners do not bind it. No baked Qwen; `proxy_model` /
# `proxy_models` stay empty. Missing/closed/judge down → 503.
#
# Eval executor: the machine the digest-pinned proof-eval image is rented on.
# These are ceilings on the live `EvalExecutorOffer` (operator state,
# `PROOF_EVAL_EXECUTOR_OFFER_FILE`, rotated via POST /v1/admin/proof/executor)
# — a sibling of the RLM judge InferenceOffer, never the same document.
# `gpu_class = "1x"` is the only shape harvest may rent (rent_gpu_count ≠ 1
# aborts before the rent). An offer may declare a shorter proof deadline than
# `max_proof_deadline_s_ceiling`, never longer; a topic may tighten it again
# (`eval_executor.max_proof_deadline_s`) and may pin the live offer's
# `config_commitment` (`eval_executor.require_offer_commitment`). No per-topic
# machine id. The offer's `lium_template_id` is always the digest-scoped
# template name (`proof-eval-<12 hex of the digest>`, resolved bound to
# eval_image@digest); a raw Lium template UUID is refused under any allowlist.
# `allowed_lium_template_prefixes` is optional; when set, the name must also
# carry one of the prefixes. Missing / closed / shape ≠ 1x → can_score=false
# → 503.
#
# Holdout size / stratum size are the measurement harness (5 scored splits × 24),
# not the problem list.

Expand All @@ -49,6 +65,11 @@ allowed_modes = ["chat", "completions", "embeddings"]
max_input_tokens_ceiling = 32768
max_output_tokens_ceiling = 8192
inference_offer_commitment_alg = "sha256"
eval_executor_schema_version = 1
gpu_class = "1x"
max_proof_deadline_s_ceiling = 7200
allowed_lium_template_prefixes = ["proof-eval-"]
eval_executor_commitment_alg = "sha256"
eval_image = "ghcr.io/cortexlm/proof-eval"
eval_image_digest = "sha256:78b614a1f51ce5dd80076c4e343a2b31b85d6c36025e02836cb83929867e7009"
proof_git = "https://github.com/CortexLM/cortex"
Expand Down
Loading
Loading