use crate::adversarial::defender::{Defendant, DefendantCatalog};
use crate::pipeline::certify::CertificateStrength;
use crate::proof::algebra::checker::{verify_laws, verify_laws_witnessed};
use crate::spec::law::{canonical_law_id, AlgebraicLaw};
use crate::OpSpec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GauntletFinding {
pub defendant_id: String,
pub target_op_id: String,
pub laws_violated: Vec<String>,
pub over_catches: Vec<String>,
pub escaped_laws: Vec<String>,
pub witness_failed: bool,
pub caught: bool,
pub status: GauntletStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GauntletStatus {
Executed,
Malformed {
reason: String,
},
SkippedMissingTarget,
Imprecise {
reason: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct GauntletReport {
pub findings: Vec<GauntletFinding>,
}
impl GauntletReport {
#[inline]
pub fn escaped(&self) -> Vec<&GauntletFinding> {
self.findings
.iter()
.filter(|f| {
!f.caught
&& matches!(
f.status,
GauntletStatus::Executed | GauntletStatus::Imprecise { .. }
)
})
.collect()
}
#[inline]
pub fn caught(&self) -> Vec<&GauntletFinding> {
self.findings.iter().filter(|f| f.caught).collect()
}
}
fn law_fingerprints(law: &AlgebraicLaw) -> Vec<String> {
let mut fingerprints = vec![canonical_law_id(law)];
if let AlgebraicLaw::Custom { name, .. } = law {
fingerprints.push(format!("custom({name})"));
}
fingerprints
}
#[inline]
pub fn run_defendant(
defendant: &Defendant,
spec_cpu_fn: fn(&[u8]) -> Vec<u8>,
declared_laws: &[AlgebraicLaw],
is_binary: bool,
) -> GauntletFinding {
run_defendant_with_strength(
defendant,
spec_cpu_fn,
declared_laws,
is_binary,
CertificateStrength::FastCheck,
)
}
#[inline]
pub fn run_defendant_with_strength(
defendant: &Defendant,
spec_cpu_fn: fn(&[u8]) -> Vec<u8>,
declared_laws: &[AlgebraicLaw],
is_binary: bool,
strength: CertificateStrength,
) -> GauntletFinding {
let expected: Vec<String> = defendant
.fails_laws
.iter()
.map(|s| (*s).to_string())
.collect();
if expected.is_empty() && defendant.expected_witness.is_empty() {
return GauntletFinding {
defendant_id: defendant.id.to_string(),
target_op_id: defendant.target_op_id.to_string(),
laws_violated: Vec::new(),
over_catches: Vec::new(),
escaped_laws: Vec::new(),
witness_failed: false,
caught: false,
status: GauntletStatus::Malformed {
reason: "defendant declares no fails_laws and no expected_witness. Fix: add at least one law fingerprint or concrete witness.".to_string(),
},
};
}
let checked_laws: Vec<AlgebraicLaw> = declared_laws.to_vec();
let results = if matches!(strength, CertificateStrength::FastCheck) {
verify_laws(
defendant.target_op_id,
defendant.broken_fn,
&checked_laws,
is_binary,
)
} else {
verify_laws_witnessed(
defendant.target_op_id,
defendant.broken_fn,
&checked_laws,
is_binary,
strength.witness_count(),
)
};
let mut violated_fingerprints: Vec<String> = Vec::new();
for (law, result) in checked_laws.iter().zip(results.iter()) {
if result.violation.is_some() {
violated_fingerprints.extend(law_fingerprints(law));
}
}
violated_fingerprints.sort();
violated_fingerprints.dedup();
let escaped_laws: Vec<String> = expected
.iter()
.filter(|e| !violated_fingerprints.contains(*e))
.cloned()
.collect();
let over_catches: Vec<String> = violated_fingerprints
.iter()
.filter(|v| !expected.contains(v))
.cloned()
.collect();
let witness_failed = defendant.expected_witness.iter().any(|(a, b)| {
let mut input = Vec::with_capacity(if is_binary { 8 } else { 4 });
input.extend_from_slice(&a.to_le_bytes());
if is_binary {
input.extend_from_slice(&b.to_le_bytes());
}
(defendant.broken_fn)(&input) != spec_cpu_fn(&input)
});
let caught = (!expected.is_empty() && escaped_laws.is_empty()) || witness_failed;
let status = if over_catches.len() > expected.len().saturating_mul(2).max(2) {
GauntletStatus::Imprecise {
reason: format!(
"defendant tripped {} undeclared laws for {} declared laws. Fix: narrow the sabotage or declare the additional targeted laws.",
over_catches.len(),
expected.len()
),
}
} else {
GauntletStatus::Executed
};
GauntletFinding {
defendant_id: defendant.id.to_string(),
target_op_id: defendant.target_op_id.to_string(),
laws_violated: violated_fingerprints,
over_catches,
escaped_laws,
witness_failed,
caught,
status,
}
}
#[inline]
pub fn run_gauntlet(catalogs: &[DefendantCatalog], specs: &[OpSpec]) -> GauntletReport {
let mut report = GauntletReport::default();
for catalog in catalogs {
let Some(spec) = specs.iter().find(|s| s.id == catalog.target_op_id) else {
for defendant in &catalog.defendants {
report.findings.push(GauntletFinding {
defendant_id: defendant.id.to_string(),
target_op_id: defendant.target_op_id.to_string(),
laws_violated: Vec::new(),
over_catches: Vec::new(),
escaped_laws: defendant
.fails_laws
.iter()
.map(|s| (*s).to_string())
.collect(),
witness_failed: false,
caught: false,
status: GauntletStatus::SkippedMissingTarget,
});
}
continue;
};
let is_binary = spec.signature.inputs.len() == 2;
for defendant in &catalog.defendants {
let finding = run_defendant(defendant, spec.cpu_fn, &spec.laws, is_binary);
report.findings.push(finding);
}
}
report
}