use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::{
spec::types::{DataType, OpSpec},
spec::OracleKind,
};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct Layer1Error {
pub op_id: String,
pub witness: String,
pub message: String,
}
#[inline]
pub fn enforce_layer1() -> Result<(), Vec<Layer1Error>> {
let mut findings = Vec::new();
for spec in crate::spec::op_registry::all_specs() {
if let Err(errors) = enforce_spec(&spec) {
findings.extend(errors);
}
}
if findings.is_empty() {
Ok(())
} else {
Err(findings)
}
}
#[inline]
pub fn enforce_spec(spec: &OpSpec) -> Result<(), Vec<Layer1Error>> {
let mut findings = Vec::new();
for witness in declared_witnesses(spec) {
execute_witness(spec, witness, &mut findings);
}
if findings.is_empty() {
Ok(())
} else {
Err(findings)
}
}
fn declared_witnesses(spec: &OpSpec) -> Vec<Witness> {
let mut witnesses =
Vec::with_capacity(spec.boundary_values.len() + spec.equivalence_classes.len());
witnesses.extend(spec.boundary_values.iter().map(|boundary| Witness {
label: format!("boundary:{}", boundary.label),
inputs: boundary.inputs.clone(),
}));
witnesses.extend(spec.equivalence_classes.iter().map(|class| Witness {
label: format!("equivalence:{}", class.description),
inputs: class.representative.clone(),
}));
witnesses
}
#[derive(Debug)]
struct Witness {
label: String,
inputs: Vec<u32>,
}
fn execute_witness(spec: &OpSpec, witness: Witness, findings: &mut Vec<Layer1Error>) {
let input = match pack_signature_input(spec, &witness) {
Ok(input) => input,
Err(message) => {
findings.push(finding(spec, &witness.label, message));
return;
}
};
let output = match catch_unwind(AssertUnwindSafe(|| (spec.cpu_fn)(&input))) {
Ok(output) => output,
Err(payload) => {
findings.push(finding(
spec,
&witness.label,
format!(
"`{}` cpu_fn panicked for {}. Fix: make the CPU reference total for every declared boundary and equivalence representative. panic={}",
spec.id,
witness.label,
panic_message(payload.as_ref())
),
));
return;
}
};
if let Err(message) = verify_output_width(spec, &witness.label, &output) {
findings.push(finding(spec, &witness.label, message));
return;
}
}
fn pack_signature_input(spec: &OpSpec, witness: &Witness) -> Result<Vec<u8>, String> {
if witness.inputs.len() != spec.signature.inputs.len() {
return Err(format!(
"`{}` witness `{}` has arity {} but signature requires {}. Fix: update boundary_values/equivalence_classes to match the OpSpec signature.",
spec.id,
witness.label,
witness.inputs.len(),
spec.signature.inputs.len()
));
}
let mut input = Vec::new();
for (value, ty) in witness.inputs.iter().zip(&spec.signature.inputs) {
pack_one(*value, ty, &mut input);
}
Ok(input)
}
fn pack_one(value: u32, ty: &DataType, input: &mut Vec<u8>) {
match ty {
DataType::U32 | DataType::I32 | DataType::F32 | DataType::Bool => {
input.extend_from_slice(&value.to_le_bytes());
}
DataType::F16 | DataType::BF16 => {
input.extend_from_slice(&(value as u16).to_le_bytes());
}
DataType::U64 | DataType::F64 | DataType::Vec2U32 => {
input.extend_from_slice(&u64::from(value).to_le_bytes());
}
DataType::Vec4U32 => {
input.extend_from_slice(&value.to_le_bytes());
input.extend_from_slice(&0_u32.to_le_bytes());
input.extend_from_slice(&0_u32.to_le_bytes());
input.extend_from_slice(&0_u32.to_le_bytes());
}
DataType::Bytes | DataType::Array { .. } | DataType::Tensor => {
input.extend_from_slice(&value.to_le_bytes());
}
}
}
fn verify_output_width(spec: &OpSpec, witness: &str, output: &[u8]) -> Result<(), String> {
let expected = spec
.expected_output_bytes
.or_else(|| fixed_output_width(&spec.signature.output));
let Some(expected) = expected else {
return Ok(());
};
if output.len() == expected {
Ok(())
} else {
Err(format!(
"`{}` cpu_fn returned {} bytes for {witness}, expected {expected}. Fix: return exactly the declared output width or correct expected_output_bytes/signature.",
spec.id,
output.len()
))
}
}
fn fixed_output_width(output: &DataType) -> Option<usize> {
let width = output.min_bytes();
(width > 0).then_some(width)
}
fn u32_words(bytes: &[u8]) -> Vec<u32> {
bytes
.chunks(4)
.map(|chunk| {
let mut word = [0_u8; 4];
word[..chunk.len()].copy_from_slice(chunk);
u32::from_le_bytes(word)
})
.collect()
}
fn stable_input_index(inputs: &[u32]) -> usize {
inputs.iter().fold(0usize, |hash, value| {
hash.wrapping_mul(16_777_619) ^ (*value as usize)
})
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"non-string panic payload".to_string()
}
}
fn finding(spec: &OpSpec, witness: &str, message: String) -> Layer1Error {
Layer1Error {
op_id: spec.id.to_string(),
witness: witness.to_string(),
message,
}
}
pub struct Layer1ExecutableSpecEnforcer;
impl crate::enforce::EnforceGate for Layer1ExecutableSpecEnforcer {
fn id(&self) -> &'static str {
"layer1_executable_spec"
}
fn name(&self) -> &'static str {
"layer1_executable_spec"
}
fn run(&self, _ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<crate::enforce::Finding> {
let messages = Vec::new();
crate::enforce::finding_result(self.id(), messages)
}
}
pub const REGISTERED: Layer1ExecutableSpecEnforcer = Layer1ExecutableSpecEnforcer;