use exo_core::types::Hash256;
use serde::{Deserialize, Serialize};
use crate::error::{ProofError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProofStatementKind {
GovernanceCompliance,
DagInclusion,
ExecutionReceipt,
ModelInference,
PedagogicalCompatibility,
}
pub const UNAUDITED_BLAKE3_STANDIN_BACKEND_ID: BackendId = BackendId::UnauditedBlake3Standin;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackendId {
UnauditedBlake3Standin,
RiscZero,
Unknown(u32),
}
impl BackendId {
#[must_use]
pub const fn is_registered(&self) -> bool {
!matches!(self, BackendId::Unknown(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditStatus {
Pedagogical,
PendingExternalReview,
ProductionReviewed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackendDescriptor {
pub backend_id: BackendId,
pub audit_status: AuditStatus,
}
#[must_use]
pub fn default_registry() -> Vec<BackendDescriptor> {
vec![
BackendDescriptor {
backend_id: BackendId::UnauditedBlake3Standin,
audit_status: AuditStatus::Pedagogical,
},
BackendDescriptor {
backend_id: BackendId::RiscZero,
audit_status: AuditStatus::PendingExternalReview,
},
]
}
pub trait RiscZeroReceiptVerifier {
fn verify_receipt(
&self,
receipt_bytes: &[u8],
image_id_or_verifier_key: &[u8],
journal_digest: &Hash256,
) -> Result<bool>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FailClosedRiscZeroVerifier;
impl RiscZeroReceiptVerifier for FailClosedRiscZeroVerifier {
fn verify_receipt(
&self,
_receipt_bytes: &[u8],
_image_id_or_verifier_key: &[u8],
_journal_digest: &Hash256,
) -> Result<bool> {
Err(ProofError::VerificationFailed(
"BackendId::RiscZero verifier is a pending-review scaffold: no external \
cryptographic review of the risc0 verify path has landed yet, so this refuses \
closed rather than trust an unaudited verifier. See \
RiscZeroReceiptVerifier for exactly where the audited risc0 verify call plugs \
in once review lands."
.to_string(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofEnvelope {
pub statement_kind: ProofStatementKind,
pub backend_id: BackendId,
pub version: u32,
pub public_inputs: Vec<Vec<u8>>,
pub commitment_roots: Vec<Hash256>,
pub verifier_key_or_image_id: Vec<u8>,
pub domain_separator: Vec<u8>,
}
const ENVELOPE_BINDING_DOMAIN: &str = "exo-proofs:envelope-binding:v1";
#[derive(Serialize)]
struct EnvelopeBinding<'a> {
domain: &'a str,
statement_kind: &'a ProofStatementKind,
backend_id: &'a BackendId,
version: u32,
public_inputs: &'a [Vec<u8>],
commitment_roots: &'a [Hash256],
verifier_key_or_image_id: &'a [u8],
domain_separator: &'a [u8],
}
impl ProofEnvelope {
pub fn validate_backend(&self) -> Result<()> {
if self.backend_id.is_registered() {
Ok(())
} else {
Err(ProofError::InvalidProofFormat(format!(
"proof envelope names unknown/unregistered backend id: {:?}",
self.backend_id
)))
}
}
pub fn binding_digest(&self) -> Result<Hash256> {
let binding = EnvelopeBinding {
domain: ENVELOPE_BINDING_DOMAIN,
statement_kind: &self.statement_kind,
backend_id: &self.backend_id,
version: self.version,
public_inputs: &self.public_inputs,
commitment_roots: &self.commitment_roots,
verifier_key_or_image_id: &self.verifier_key_or_image_id,
domain_separator: &self.domain_separator,
};
let mut encoded = Vec::new();
ciborium::into_writer(&binding, &mut encoded).map_err(|err| {
ProofError::InvalidProofFormat(format!(
"failed to canonical-CBOR encode envelope binding for digest: {err}"
))
})?;
Ok(Hash256(*blake3::hash(&encoded).as_bytes()))
}
fn verify_riscz(
&self,
receipt_bytes: &[u8],
verifier: &dyn RiscZeroReceiptVerifier,
) -> Result<bool> {
let journal_digest = self.binding_digest()?;
verifier.verify_receipt(
receipt_bytes,
&self.verifier_key_or_image_id,
&journal_digest,
)
}
pub fn verify(&self, receipt_bytes: &[u8]) -> Result<bool> {
self.validate_backend()?;
match self.backend_id {
BackendId::UnauditedBlake3Standin => {
crate::guard_unaudited("envelope::ProofEnvelope::verify")?;
Err(ProofError::VerificationFailed(format!(
"no verifier is wired for backend {:?} yet; \
ProofEnvelope::verify is a fail-closed stub until \
VCG-001b lands real per-backend verification",
self.backend_id
)))
}
BackendId::RiscZero => {
self.verify_riscz(receipt_bytes, &FailClosedRiscZeroVerifier)
}
BackendId::Unknown(_) => unreachable!(
"validate_backend() above must have already refused an unregistered backend id"
),
}
}
}
#[cfg(test)]
mod canonical_encoding_contract_tests {
#[test]
fn envelope_module_uses_canonical_cbor_not_json() {
let source = include_str!("envelope.rs");
let production = source
.split("// ---------------------------------------------------------------------------\n// Tests")
.next()
.expect("production section exists");
assert!(
!production.contains("serde_json"),
"envelope module must not use serde_json anywhere in its production code path"
);
}
#[test]
fn backend_id_unknown_variant_is_the_only_unregistered_case() {
use super::BackendId;
assert!(BackendId::UnauditedBlake3Standin.is_registered());
assert!(!BackendId::Unknown(0).is_registered());
assert!(!BackendId::Unknown(u32::MAX).is_registered());
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod riscz_seam_binding_unit {
use std::cell::RefCell;
use super::*;
struct SpyVerifier {
seen_digest: RefCell<Option<Hash256>>,
}
impl RiscZeroReceiptVerifier for SpyVerifier {
fn verify_receipt(
&self,
_receipt_bytes: &[u8],
_image_id_or_verifier_key: &[u8],
journal_digest: &Hash256,
) -> Result<bool> {
*self.seen_digest.borrow_mut() = Some(*journal_digest);
Err(ProofError::VerificationFailed(
"spy verifier records the digest only; it never manufactures success".to_string(),
))
}
}
fn riscz_env() -> ProofEnvelope {
ProofEnvelope {
statement_kind: ProofStatementKind::ExecutionReceipt,
backend_id: BackendId::RiscZero,
version: 1,
public_inputs: vec![b"pi".to_vec()],
commitment_roots: vec![Hash256([3u8; 32])],
verifier_key_or_image_id: b"img".to_vec(),
domain_separator: b"dom".to_vec(),
}
}
#[test]
fn seam_passes_binding_digest_to_verifier() {
let env = riscz_env();
let spy = SpyVerifier {
seen_digest: RefCell::new(None),
};
let _ = env.verify_riscz(b"some-bytes", &spy);
assert_eq!(
*spy.seen_digest.borrow(),
Some(env.binding_digest().expect("binding digest")),
"the seam must hand the verifier exactly the envelope's binding digest"
);
}
#[test]
fn seam_binding_digest_reflects_domain_separator_change() {
let mut env = riscz_env();
let spy1 = SpyVerifier {
seen_digest: RefCell::new(None),
};
let _ = env.verify_riscz(b"receipt-bytes-a", &spy1);
env.domain_separator = b"dom-CHANGED".to_vec();
let spy2 = SpyVerifier {
seen_digest: RefCell::new(None),
};
let _ = env.verify_riscz(b"receipt-bytes-a", &spy2);
assert!(
spy1.seen_digest.borrow().is_some(),
"spy1 verifier must have been called"
);
assert_ne!(
*spy1.seen_digest.borrow(),
*spy2.seen_digest.borrow(),
"the digest reaching the verifier must change with domain_separator"
);
}
struct ReceiptBytesSpyVerifier {
seen_receipt_bytes: RefCell<Option<Vec<u8>>>,
}
impl RiscZeroReceiptVerifier for ReceiptBytesSpyVerifier {
fn verify_receipt(
&self,
receipt_bytes: &[u8],
_image_id_or_verifier_key: &[u8],
_journal_digest: &Hash256,
) -> Result<bool> {
*self.seen_receipt_bytes.borrow_mut() = Some(receipt_bytes.to_vec());
Err(ProofError::VerificationFailed(
"spy verifier records the receipt bytes only; it never manufactures success"
.to_string(),
))
}
}
#[test]
fn verify_riscz_passes_receipt_bytes_to_seam() {
let env = riscz_env();
let spy = ReceiptBytesSpyVerifier {
seen_receipt_bytes: RefCell::new(None),
};
let _ = env.verify_riscz(b"distinct-receipt-bytes", &spy);
assert_eq!(
spy.seen_receipt_bytes.borrow().as_deref(),
Some(b"distinct-receipt-bytes".as_slice()),
"the seam must hand the verifier exactly the receipt bytes it was called with"
);
}
#[test]
fn verify_riscz_receipt_bytes_reflect_the_argument_change() {
let env = riscz_env();
let spy1 = ReceiptBytesSpyVerifier {
seen_receipt_bytes: RefCell::new(None),
};
let _ = env.verify_riscz(b"receipt-bytes-one", &spy1);
let spy2 = ReceiptBytesSpyVerifier {
seen_receipt_bytes: RefCell::new(None),
};
let _ = env.verify_riscz(b"receipt-bytes-TWO", &spy2);
assert!(
spy1.seen_receipt_bytes.borrow().is_some(),
"spy1 verifier must have been called"
);
assert_ne!(
*spy1.seen_receipt_bytes.borrow(),
*spy2.seen_receipt_bytes.borrow(),
"the receipt bytes reaching the verifier must change with the argument passed"
);
}
}