Skip to main content

molframe_core/contract/
reexecution.rs

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