Skip to main content

rustev_eval/
remote_evidence.rs

1//! Remote evidence availability (spec 020, 3.2).
2//!
3//! A `rustev.run/2` record references each remote attempt's exchange
4//! record; it never embeds one. Replay reproduces from retained raw outputs
5//! (spec 004) and never reads an exchange record, makes a network request
6//! or regenerates a value from one. Separately, and only when asked, this
7//! module checks the references a run record holds against a host-supplied,
8//! synchronous [`ExchangeStore`]: each is `available` (the bytes are the
9//! bounded exchange record the digest names), or unavailable with an
10//! explicit reason. Availability is reported apart from judgment parity and
11//! never changes it.
12
13use std::panic::{AssertUnwindSafe, catch_unwind};
14
15use rustev_contract::Document;
16use rustev_contract::limits::EXCHANGE_V1;
17use rustev_contract::remote::ExchangeRecord;
18use rustev_contract::run::{EvidenceUnavailable, ExchangeReference, RunRecord};
19
20/// What a host returns for an exchange record locator.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ExchangeLookup {
23    /// At most the `max_bytes` asked for.
24    Bytes(Vec<u8>),
25    Missing,
26    /// Past the host's retention.
27    Expired,
28    /// The caller may not read it.
29    Forbidden,
30}
31
32/// The host's synchronous exchange-record store. `locator` is opaque, never
33/// an instruction to fetch a URL; the host bounds its read by `max_bytes`.
34pub trait ExchangeStore {
35    fn lookup(&self, locator: &str, max_bytes: usize) -> ExchangeLookup;
36}
37
38/// The availability of one attempt's exchange record.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum RemoteAvailability {
41    Available,
42    Missing,
43    Expired,
44    Forbidden,
45    /// Bytes that are not the bounded exchange record the digest names.
46    Corrupt,
47    /// The store failed or panicked.
48    Inaccessible,
49    /// The run record itself states why there is no reference.
50    NotReferenced(EvidenceUnavailable),
51}
52
53/// One attempt's availability.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct AttemptAvailability {
56    pub attempt_id: String,
57    pub availability: RemoteAvailability,
58}
59
60/// The availability of every remote evidence entry of `run`, in its order;
61/// empty for a record without remote evidence. Reads only through `store`.
62pub fn availability(run: &RunRecord, store: &dyn ExchangeStore) -> Vec<AttemptAvailability> {
63    let Some(entries) = &run.remote_evidence else {
64        return vec![];
65    };
66    entries
67        .iter()
68        .map(|e| AttemptAvailability {
69            attempt_id: e.attempt_id.clone(),
70            availability: match &e.exchange {
71                ExchangeReference::Unavailable { reason } => {
72                    RemoteAvailability::NotReferenced(*reason)
73                }
74                ExchangeReference::Retained {
75                    digest,
76                    schema,
77                    locator,
78                    ..
79                } => check(store, locator, schema, digest),
80            },
81        })
82        .collect()
83}
84
85fn check(
86    store: &dyn ExchangeStore,
87    locator: &str,
88    schema: &str,
89    digest: &str,
90) -> RemoteAvailability {
91    if schema != <ExchangeRecord as Document>::SCHEMA {
92        return RemoteAvailability::Corrupt;
93    }
94    let max = EXCHANGE_V1.max_bytes;
95    let found = catch_unwind(AssertUnwindSafe(|| store.lookup(locator, max)));
96    match found {
97        Err(_) => RemoteAvailability::Inaccessible,
98        Ok(ExchangeLookup::Missing) => RemoteAvailability::Missing,
99        Ok(ExchangeLookup::Expired) => RemoteAvailability::Expired,
100        Ok(ExchangeLookup::Forbidden) => RemoteAvailability::Forbidden,
101        Ok(ExchangeLookup::Bytes(b)) if b.len() > max => RemoteAvailability::Corrupt,
102        Ok(ExchangeLookup::Bytes(b)) => match ExchangeRecord::parse_checked(&b) {
103            Ok(r)
104                if r.record_digest().ok().map(|d| d.as_str().to_string())
105                    == Some(digest.to_string())
106                    && r.record_canonical().ok().as_deref() == Some(&b[..]) =>
107            {
108                RemoteAvailability::Available
109            }
110            _ => RemoteAvailability::Corrupt,
111        },
112    }
113}