use exo_core::types::Hash256;
use risc0_zkvm::{Digest, Receipt};
use crate::{
envelope::RiscZeroReceiptVerifier,
error::{ProofError, Result},
};
#[derive(Debug, Clone, Copy, Default)]
pub struct Risc0Groth16Verifier;
impl RiscZeroReceiptVerifier for Risc0Groth16Verifier {
fn verify_receipt(
&self,
receipt_bytes: &[u8],
image_id_or_verifier_key: &[u8],
journal_digest: &Hash256,
) -> Result<bool> {
let receipt: Receipt = ciborium::from_reader(receipt_bytes).map_err(|err| {
ProofError::DeserializationError(format!(
"failed to decode receipt_bytes as a canonical-CBOR risc0 Receipt: {err}"
))
})?;
let image_id = Digest::try_from(image_id_or_verifier_key).map_err(|err| {
ProofError::InvalidProofFormat(format!(
"image_id_or_verifier_key is not a valid 32-byte risc0 image id \
digest (expected {} bytes): {err}",
risc0_zkvm::sha::DIGEST_BYTES
))
})?;
receipt.verify(image_id).map_err(|err| {
ProofError::VerificationFailed(format!("risc0 receipt failed to verify: {err}"))
})?;
let actual_journal_digest = Hash256(*blake3::hash(&receipt.journal.bytes).as_bytes());
if &actual_journal_digest != journal_digest {
return Err(ProofError::VerificationFailed(format!(
"risc0 receipt journal does not commit to the expected envelope binding \
digest: receipt journal hashes to {actual_journal_digest:?}, expected \
{journal_digest:?}. A receipt proven for one envelope context must never \
verify under another."
)));
}
Ok(true)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use exo_core::types::Hash256;
use super::Risc0Groth16Verifier;
use crate::envelope::RiscZeroReceiptVerifier;
fn verifier() -> Risc0Groth16Verifier {
Risc0Groth16Verifier
}
#[test]
fn empty_receipt_bytes_are_rejected() {
let result = verifier().verify_receipt(&[], &[0u8; 32], &Hash256([0u8; 32]));
assert!(
result.is_err(),
"empty receipt_bytes must fail closed, got {result:?}"
);
}
#[test]
fn garbage_receipt_bytes_are_rejected() {
let garbage = vec![0xFFu8; 256];
let result = verifier().verify_receipt(&garbage, &[0u8; 32], &Hash256([0u8; 32]));
assert!(
result.is_err(),
"garbage (non-CBOR, non-Receipt) receipt_bytes must fail closed, got {result:?}"
);
}
#[test]
fn truncated_cbor_receipt_bytes_are_rejected() {
let truncated: Vec<u8> = vec![0xA1, 0x64, b't', b'e', b's', b't'];
let result = verifier().verify_receipt(&truncated, &[0u8; 32], &Hash256([0u8; 32]));
assert!(
result.is_err(),
"truncated/malformed CBOR must fail closed, got {result:?}"
);
}
#[test]
fn wrong_length_image_id_is_rejected_even_with_empty_receipt() {
let too_short = vec![0u8; 4];
let result = verifier().verify_receipt(&[], &too_short, &Hash256([0u8; 32]));
assert!(
result.is_err(),
"wrong-length image id must fail closed, got {result:?}"
);
}
#[test]
fn wrong_length_image_id_is_rejected_independent_of_decode_error_message() {
let too_long = vec![0u8; 64];
let garbage_receipt = vec![0x00u8; 16];
let result = verifier().verify_receipt(&garbage_receipt, &too_long, &Hash256([0u8; 32]));
assert!(
result.is_err(),
"wrong-length image id combined with malformed receipt bytes must still fail \
closed, got {result:?}"
);
}
#[test]
#[ignore = "needs a committed real-receipt fixture (follow-up, see module docs): \
risc0-zkvm 3.0.5 ships no serialized Receipt/Groth16Receipt test vector \
reachable without the excluded 'prove' feature, and this test must not \
fake acceptance. Once a real, small, committable Groth16 receipt fixture \
exists (generated out-of-band with the full prove stack and committed as \
a binary fixture), this test should decode it, call verify_receipt with \
its true image id and journal digest, and assert Ok(true)."]
fn accepts_a_genuinely_valid_groth16_receipt() {
unimplemented!("deferred: requires a committed real-receipt fixture; see #[ignore] reason")
}
}