vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Public construction helpers for operation chains.
//!
//! Every `ChainSpec` carries a [`ProofToken`] that encodes which algebraic
//! laws survive the composition. The token is load-bearing: the dispatch
//! pipeline refuses to lower any chain whose attached token does not
//! match one recomputed from the chain's own specs.
//!
//! # The trust boundary
//!
//! The blessed constructor is [`ChainSpec::from_specs`] — it derives the
//! proof token internally from the provided specs, so a caller cannot
//! attach a bogus token that claims preserved laws the specs do not
//! actually compose to. Every in-crate call site and every downstream
//! user should prefer `from_specs`.
//!
//! [`ChainSpec::new`] is kept as a legacy constructor so pre-existing
//! callers do not break, but it now returns `Err` when the passed token
//! does not match the derived one. The pipeline's runtime
//! `pipeline::proof::validate_chain` still catches any mismatch before
//! dispatch, so there is no path from a chain with a corrupted proof to
//! a GPU dispatch — the construction-time check is the early-warning
//! system, and the pipeline check is the floor.

use crate::spec::types::{
    ChainSpec, ConstructionTime, CpuReferenceFn, OpSignature, OpSpec, ProofToken, ProofTokenError,
};

impl ChainSpec {
    /// Build a chain specification, deriving the composition proof token
    /// from the provided specs. This is the blessed constructor: it is
    /// impossible for a caller to attach a bogus proof because the token
    /// is computed here from the authoritative source (`specs`).
    ///
    /// Prefer this over [`ChainSpec::new`] in every new call site.
    #[inline]
    pub fn from_specs(
        id: String,
        ops: Vec<&'static str>,
        signature: OpSignature,
        specs: Vec<OpSpec>,
        cpu_chain: Option<CpuReferenceFn>,
    ) -> Result<Self, ProofTokenError> {
        validate_ops_match_specs(&id, &ops, &specs)?;
        let proof_token = ProofToken::from_specs(&specs, ConstructionTime::Manual)?;
        Ok(Self {
            id,
            ops,
            signature,
            specs,
            cpu_chain,
            proof_token,
        })
    }

    /// Construct a chain with an explicit, possibly-wrong, proof token
    /// — bypassing the construction-time check entirely.
    ///
    /// **This is an escape hatch.** It exists for two narrow use cases:
    ///
    /// 1. **Testing the runtime gate.** A test that wants to construct
    ///    a deliberately-wrong chain to verify the dispatch-time
    ///    `pipeline::proof::validate_chain` catches it must bypass the
    ///    construction-time assert; otherwise the assert fires before
    ///    the runtime gate has a chance to run.
    /// 2. **Forensic replay.** When loading a serialized chain whose
    ///    proof token may legitimately differ from a recomputed one
    ///    (e.g., the spec set has changed since the chain was frozen),
    ///    the loader needs to reconstruct the chain as-was without
    ///    asserting on a token that no longer matches.
    ///
    /// Production code paths and contributors writing new ops MUST use
    /// [`ChainSpec::from_specs`]. Reaching for `new_unchecked` outside
    /// the two cases above is a code smell — it deliberately disables
    /// the construction-time discipline that makes the proof system
    /// honest.
    #[must_use]
    #[inline]
    pub fn new_unchecked(
        id: String,
        ops: Vec<&'static str>,
        signature: OpSignature,
        specs: Vec<OpSpec>,
        cpu_chain: Option<CpuReferenceFn>,
        proof_token: ProofToken,
    ) -> Self {
        Self {
            id,
            ops,
            signature,
            specs,
            cpu_chain,
            proof_token,
        }
    }

    /// Legacy constructor: take an explicit construction-time proof.
    ///
    /// This verifies the passed token equals `ProofToken::from_specs(&specs, _)`.
    /// A caller that constructs a wrong token fails fast at the call site
    /// rather than shipping a chain that would silently fail the pipeline's
    /// proof check later.
    ///
    /// New code should use [`ChainSpec::from_specs`] instead.
    #[inline]
    pub fn new(
        id: String,
        ops: Vec<&'static str>,
        signature: OpSignature,
        specs: Vec<OpSpec>,
        cpu_chain: Option<CpuReferenceFn>,
        proof_token: ProofToken,
    ) -> Result<Self, ProofTokenError> {
        validate_ops_match_specs(&id, &ops, &specs)?;
        let expected = ProofToken::from_specs(&specs, proof_token.computed_at)?;
        if !proof_token.theorem_claims_match(&expected) {
            return Err(ProofTokenError::VerificationFailed(format!(
                "ChainSpec::new: proof token mismatch for chain `{id}`. Fix: use \
                 ChainSpec::from_specs which derives the token from specs, or \
                 recompute ProofToken::from_specs(&specs, _) before passing."
            )));
        }
        Ok(Self {
            id,
            ops,
            signature,
            specs,
            cpu_chain,
            proof_token,
        })
    }
}

fn validate_ops_match_specs(
    chain_id: &str,
    ops: &[&'static str],
    specs: &[OpSpec],
) -> Result<(), ProofTokenError> {
    if ops.len() != specs.len() {
        return Err(ProofTokenError::VerificationFailed(format!(
            "ChainSpec `{chain_id}` has {} op ids but {} specs. Fix: pass one op id for each spec, in the same order.",
            ops.len(),
            specs.len()
        )));
    }
    for (index, (op_id, spec)) in ops.iter().zip(specs.iter()).enumerate() {
        if *op_id != spec.id {
            return Err(ProofTokenError::VerificationFailed(format!(
                "ChainSpec `{chain_id}` op id mismatch at index {index}: ops has `{op_id}` but specs has `{}`. Fix: build chains with ops[i] == specs[i].id.",
                spec.id
            )));
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {

    use crate::spec::types::{ChainSpec, ConstructionTime, OpSignature, OpSpec, ProofToken};

    fn empty_signature() -> OpSignature {
        OpSignature {
            inputs: Vec::new(),
            output: crate::spec::types::DataType::U32,
        }
    }

    fn dummy_cpu(_input: &[u8]) -> Vec<u8> {
        vec![0, 0, 0, 0]
    }

    fn dummy_wgsl() -> String {
        String::new()
    }

    #[test]
    fn from_specs_derives_proof_token_internally() {
        let chain = ChainSpec::from_specs(
            "empty-chain".to_string(),
            Vec::new(),
            empty_signature(),
            Vec::<OpSpec>::new(),
            None,
        )
        .unwrap();
        let expected = ProofToken::from_specs(&chain.specs, ConstructionTime::Manual).unwrap();
        assert!(
            chain.proof_token.theorem_claims_match(&expected),
            "ChainSpec::from_specs must attach a token equal to \
             ProofToken::from_specs(&chain.specs, _); otherwise the gate can \
             be bypassed by a wrong construction-time token."
        );
    }

    #[test]
    fn new_matches_from_specs_on_correct_token() {
        let specs = Vec::<OpSpec>::new();
        let token = ProofToken::from_specs(&specs, ConstructionTime::Manual).unwrap();
        let chain = ChainSpec::new(
            "correct-token".to_string(),
            Vec::new(),
            empty_signature(),
            specs,
            None,
            token.clone(),
        )
        .expect("ChainSpec::new must accept a correctly-derived proof token");
        assert!(chain.proof_token.theorem_claims_match(&token));
    }

    #[test]
    fn from_specs_rejects_mismatched_op_ids() {
        let spec = OpSpec::builder("primitive.math.add")
            .signature(empty_signature())
            .cpu_fn(dummy_cpu)
            .wgsl_fn(dummy_wgsl)
            .category(crate::Category::A {
                composition_of: vec!["primitive.math.add"],
            })
            .laws(Vec::new())
            .strictness(crate::spec::types::Strictness::Strict)
            .version(1)
            .build()
            .expect("Fix: test OpSpec fixture must satisfy builder invariants.");
        let result = ChainSpec::from_specs(
            "bad-chain".to_string(),
            vec!["primitive.math.sub"],
            empty_signature(),
            vec![spec],
            None,
        );
        let err = match result {
            Ok(_) => panic!("Fix: mismatched ChainSpec ops/specs must fail loudly."),
            Err(err) => err,
        };
        assert!(
            err.to_string().contains("op id mismatch"),
            "Fix: mismatched ChainSpec ops/specs must fail loudly, got {err}"
        );
    }
}