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
2 changes: 2 additions & 0 deletions Cargo.lock

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

17 changes: 17 additions & 0 deletions crates/db/migrations/0016_prism_precheck_quota.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Prism miner similarity precheck quota: 3 attempts per coldkey per UTC day.
--
-- `POST /v1/submissions/precheck` runs the same pre-LLM copy gate as intake
-- without creating a submission or renting a pod. Quota is keyed by coldkey
-- (hotkey fallback when Owner is unknown) so rotating hotkeys cannot reset
-- the daily budget.

CREATE TABLE prism_precheck_quota (
miner_coldkey TEXT NOT NULL,
day DATE NOT NULL,
checks_used INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (miner_coldkey, day),
CONSTRAINT prism_precheck_quota_key_hex CHECK (miner_coldkey ~ '^[0-9a-f]{64}$'),
CONSTRAINT prism_precheck_quota_checks_nonneg CHECK (checks_used >= 0)
);

GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE prism_precheck_quota TO base_app;
47 changes: 47 additions & 0 deletions crates/db/src/prism_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,50 @@ pub async fn stuck_prism_before_grace(
.await?;
Ok(rows)
}

/// Read precheck attempts used for `(coldkey, UTC day)` (0 when absent).
///
/// # Errors
/// SQL error.
pub async fn prism_precheck_quota_get(
pool: &PgPool,
miner_coldkey: &str,
day: &str,
) -> Result<i32, DbError> {
let row: Option<(i32,)> = sqlx::query_as(
"SELECT checks_used FROM prism_precheck_quota \
WHERE miner_coldkey = $1 AND day = $2::date",
)
.bind(miner_coldkey)
.bind(day)
.fetch_optional(pool)
.await?;
Ok(row.map_or(0, |r| r.0))
}

/// Atomically consume one precheck attempt when `checks_used < limit`.
/// Returns `Some(checks_used)` after bump, or `None` when already at limit.
///
/// # Errors
/// SQL error.
pub async fn prism_precheck_quota_try_consume(
pool: &PgPool,
miner_coldkey: &str,
day: &str,
limit: i32,
) -> Result<Option<i32>, DbError> {
let row: Option<(i32,)> = sqlx::query_as(
"INSERT INTO prism_precheck_quota (miner_coldkey, day, checks_used) \
VALUES ($1, $2::date, 1) \
ON CONFLICT (miner_coldkey, day) DO UPDATE SET \
checks_used = prism_precheck_quota.checks_used + 1 \
WHERE prism_precheck_quota.checks_used < $3 \
RETURNING checks_used",
)
.bind(miner_coldkey)
.bind(day)
.bind(limit)
.fetch_optional(pool)
.await?;
Ok(row.map(|r| r.0))
}
166 changes: 6 additions & 160 deletions crates/prism-challenge/src/agentic.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
//! Workdir + corpus helpers for the Prism agentic anti-cheat gate.
//! Workdir helpers for the Prism agentic anti-cheat gate.
//!
//! Corpus builders live in [`prism_pipeline::precheck`] (LOC split); this
//! module only materializes the review workdir.

use std::fs;
use std::path::Path;

use challenge_agentic::{
same_miner_identity, CorpusEntry, GateCorpusEntry, ReviewRequest, PRISM_DOMAIN_RULES,
};
use challenge_agentic::{CorpusEntry, ReviewRequest, PRISM_DOMAIN_RULES};
use prism_lium::{EvalReceipt, RemoteExecResult};
use prism_recipe::BASELINE_ARCHITECTURE_PY;
use prism_store::SubmissionState;

/// True when `other` is the same economic miner as `candidate` (hotkey or coldkey).
#[must_use]
pub fn same_miner(candidate: &SubmissionState, other: &SubmissionState) -> bool {
same_miner_identity(
&candidate.miner_hotkey,
candidate.miner_coldkey.as_deref(),
&other.miner_hotkey,
other.miner_coldkey.as_deref(),
)
}
pub use prism_pipeline::{corpus_from_rows, gate_corpus_from_rows, same_miner};

/// Build a temp workdir + [`ReviewRequest`] for one Prism submission.
///
Expand Down Expand Up @@ -59,148 +50,3 @@ pub fn build_review_request(
domain_rules: PRISM_DOMAIN_RULES.into(),
})
}

/// Pre-LLM copy-gate corpus: other miners' prior art only (hotkey + coldkey).
#[must_use]
pub fn gate_corpus_from_rows(
candidate: &SubmissionState,
recent: &[SubmissionState],
) -> Vec<GateCorpusEntry> {
recent
.iter()
.filter(|r| r.id != candidate.id && !same_miner(candidate, r))
.map(|r| GateCorpusEntry {
id: format!("subm:{}", r.id),
source: r.architecture_py.clone(),
created_at_ms: r.created_at_ms,
})
.collect()
}

/// Baseline + recent terminated submissions as agentic corpus entries.
///
/// Corpus entries are **architecture.py only** (similarity v2): `training.py`
/// is exempt from every copy/similarity comparison — the same training
/// script on two different architectures is legitimate competition behavior.
/// `exempt_arch` drops entries byte-equal to that source (training-only
/// submissions on a registry architecture: the identity is by design).
/// Same-hotkey and same-coldkey prior art are excluded.
#[must_use]
pub fn corpus_from_rows(
candidate: &SubmissionState,
recent: &[SubmissionState],
exempt_arch: Option<&str>,
) -> Vec<CorpusEntry> {
let mut v = vec![CorpusEntry {
id: "baseline".into(),
source: BASELINE_ARCHITECTURE_PY.into(),
}];
for r in recent {
if r.id == candidate.id || same_miner(candidate, r) {
continue;
}
if Some(r.architecture_py.as_str()) == exempt_arch {
continue;
}
let label = if r.id.len() >= 8 {
format!("subm:{}", &r.id[..8])
} else {
format!("subm:{}", r.id)
};
v.push(CorpusEntry {
id: label,
source: r.architecture_py.clone(),
});
}
v
}

#[cfg(test)]
mod tests {
use super::*;
use prism_store::{FinalScore, Stage};

fn row(
id: &str,
hotkey: &str,
coldkey: Option<&str>,
arch: &str,
created_at_ms: u64,
) -> SubmissionState {
SubmissionState {
id: id.into(),
miner_hotkey: hotkey.into(),
miner_coldkey: coldkey.map(str::to_owned),
epoch: 1,
netuid: 1,
status: Stage::Terminated,
architecture_py: arch.into(),
training_py: "train".into(),
label: None,
pod_id: None,
pod_provider: None,
receipt: None,
metrics_json: None,
bpb: Some(1.0),
arch_id: None,
review: None,
similarity: None,
final_score: Some(FinalScore::Score(1)),
retry_count: 0,
error_detail: None,
created_at_ms,
updated_at_ms: created_at_ms,
}
}

#[test]
fn same_hotkey_prior_art_excluded() {
let prior = row("aaaaaaaa", "aa", Some("11"), "arch_a", 1_000);
let next = row("bbbbbbbb", "aa", Some("11"), "arch_b", 2_000);
let recent = vec![prior, next.clone()];
assert!(gate_corpus_from_rows(&next, &recent).is_empty());
assert_eq!(
corpus_from_rows(&next, &recent, None)
.iter()
.map(|e| e.id.as_str())
.collect::<Vec<_>>(),
vec!["baseline"]
);
}

#[test]
fn same_coldkey_different_hotkey_excluded() {
let prior = row("aaaaaaaa", "aa", Some("11"), "arch_a", 1_000);
let next = row("bbbbbbbb", "bb", Some("11"), "arch_b", 2_000);
let recent = vec![prior, next.clone()];
assert!(gate_corpus_from_rows(&next, &recent).is_empty());
assert_eq!(
corpus_from_rows(&next, &recent, None)
.iter()
.map(|e| e.id.as_str())
.collect::<Vec<_>>(),
vec!["baseline"]
);
}

#[test]
fn different_coldkey_stays_in_corpus() {
let victim = row("aaaaaaaa", "aa", Some("11"), "arch_a", 1_000);
let copier = row("bbbbbbbb", "bb", Some("22"), "arch_b", 2_000);
let recent = vec![victim, copier.clone()];
assert_eq!(
gate_corpus_from_rows(&copier, &recent)
.iter()
.map(|e| e.id.as_str())
.collect::<Vec<_>>(),
vec!["subm:aaaaaaaa"]
);
assert_eq!(
corpus_from_rows(&copier, &recent, None)
.iter()
.map(|e| e.id.as_str())
.collect::<Vec<_>>(),
vec!["baseline", "subm:aaaaaaaa"]
);
}
}
Loading
Loading