Skip to main content

molframe_core/contract/
reexecution.rs

1//! Exact re-execution of an analysis from a recorded provenance record.
2//!
3//! Complexity: O(input bytes) to check the input digest, then whatever the caller's
4//! run costs; O(1) extra space.
5
6use super::{AnalysisPolicy, ContentDigest, DictionaryVersion, Provenance};
7
8/// Versions of executable and reference data available for a re-execution.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct ReexecutionEnvironment<'a> {
11    /// Installed molframe version.
12    pub molframe_version: &'a str,
13    /// Installed schema dictionary version, when one is in use.
14    pub schema_version: Option<&'a str>,
15    /// Installed chemical component dictionary version, when one is in use.
16    pub component_version: Option<&'a str>,
17}
18
19impl ReexecutionEnvironment<'static> {
20    /// Environment for an analysis with no external dictionaries.
21    #[must_use]
22    pub const fn current() -> Self {
23        Self {
24            molframe_version: env!("CARGO_PKG_VERSION"),
25            schema_version: None,
26            component_version: None,
27        }
28    }
29}
30
31/// A value recomputed under an exactly validated provenance record.
32#[derive(Clone, Debug)]
33pub struct Reexecution<T> {
34    /// Newly computed value.
35    pub value: T,
36    /// Exact record whose input, policy and reference versions were enforced.
37    pub provenance: Provenance,
38}
39
40/// Reason exact re-execution was refused.
41#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
42#[non_exhaustive]
43pub enum ReexecutionError {
44    /// The original run did not record the digest of its input bytes.
45    #[error("provenance has no input digest")]
46    MissingInputDigest,
47    /// Supplied bytes are not the bytes used by the original run.
48    #[error("input fingerprint does not match provenance")]
49    InputMismatch,
50    /// The policy record was changed without updating its fingerprint.
51    #[error("policy fingerprint does not match the recorded policy")]
52    PolicyMismatch,
53    /// Installed executable version differs from the recorded version.
54    #[error("molframe version mismatch: expected {expected}, found {observed}")]
55    SoftwareVersionMismatch {
56        /// Version recorded by the original run.
57        expected: Box<str>,
58        /// Installed version supplied by the caller.
59        observed: Box<str>,
60    },
61    /// Installed schema dictionary differs from the recorded version.
62    #[error("schema dictionary version mismatch")]
63    SchemaVersionMismatch,
64    /// Installed component dictionary differs from the recorded version.
65    #[error("component dictionary version mismatch")]
66    ComponentVersionMismatch,
67}
68
69/// Recomputes a value only after reproducing every recorded deterministic input.
70///
71/// The caller supplies bytes instead of allowing hidden filesystem or network
72/// access. Their fingerprint, the full policy, molframe version and both optional
73/// dictionary versions are checked before `run` is invoked. The callback
74/// receives the exact recorded policy rather than a reconstructed default.
75///
76/// # Errors
77///
78/// Refuses to invoke `run` if any recorded deterministic input is absent or
79/// differs from the current environment.
80pub fn reexecute_from_provenance<T>(
81    provenance: &Provenance,
82    input: &[u8],
83    environment: ReexecutionEnvironment<'_>,
84    run: impl FnOnce(&[u8], &AnalysisPolicy) -> T,
85) -> Result<Reexecution<T>, ReexecutionError> {
86    validate(provenance, input, environment)?;
87    Ok(Reexecution {
88        value: run(input, &provenance.policy),
89        provenance: provenance.clone(),
90    })
91}
92
93fn validate(
94    provenance: &Provenance,
95    input: &[u8],
96    environment: ReexecutionEnvironment<'_>,
97) -> Result<(), ReexecutionError> {
98    let Some(expected_input) = provenance.input_digest else {
99        return Err(ReexecutionError::MissingInputDigest);
100    };
101    if expected_input != ContentDigest::of(input) {
102        return Err(ReexecutionError::InputMismatch);
103    }
104    if provenance.policy_fingerprint != provenance.policy.fingerprint() {
105        return Err(ReexecutionError::PolicyMismatch);
106    }
107    if provenance.molframe_version != environment.molframe_version {
108        return Err(ReexecutionError::SoftwareVersionMismatch {
109            expected: provenance.molframe_version.into(),
110            observed: environment.molframe_version.into(),
111        });
112    }
113    check_dictionary(
114        provenance.schema_version.as_ref(),
115        environment.schema_version,
116        ReexecutionError::SchemaVersionMismatch,
117    )?;
118    check_dictionary(
119        provenance.component_version.as_ref(),
120        environment.component_version,
121        ReexecutionError::ComponentVersionMismatch,
122    )
123}
124
125fn check_dictionary(
126    expected: Option<&DictionaryVersion>,
127    observed: Option<&str>,
128    mismatch: ReexecutionError,
129) -> Result<(), ReexecutionError> {
130    if expected.map(DictionaryVersion::as_str) == observed {
131        Ok(())
132    } else {
133        Err(mismatch)
134    }
135}
136
137#[cfg(test)]
138#[path = "reexecution_tests.rs"]
139mod tests;