Skip to main content

u280_source_evidence_oracle/
u280-source-evidence-oracle.rs

1//! CLI for the independent source-simulation evidence oracle.
2
3use anyhow::{Context, Result, bail, ensure};
4use std::env;
5use std::ffi::OsString;
6use std::path::PathBuf;
7use std::process::ExitCode;
8use u280_evidence_oracle::{
9    SourceOracleConfig, SourceOracleFailure, SourceOracleOutcome, execute_source,
10};
11
12const USAGE: &str = "Usage: u280-source-evidence-oracle --source-root DIR --result-dir NEW_DIR";
13
14fn main() -> ExitCode {
15    match parse_options().and_then(|config| execute_source(&config)) {
16        Ok(outcome) => {
17            match receipt_line(&outcome) {
18                Ok(line) => println!("{line}"),
19                Err(error) => {
20                    eprintln!(
21                        "u280-source-evidence-oracle: cannot serialize result receipt: {error:#}"
22                    );
23                    return ExitCode::FAILURE;
24                }
25            }
26            if outcome.cryptographic_oracle_valid {
27                ExitCode::SUCCESS
28            } else {
29                eprintln!(
30                    "u280-source-evidence-oracle: completed comparison found a mismatch; see {}",
31                    outcome.result_directory.join("summary.json").display()
32                );
33                ExitCode::FAILURE
34            }
35        }
36        Err(error) => {
37            if let Some(failure) = error.downcast_ref::<SourceOracleFailure>() {
38                match failure_receipt_line(failure) {
39                    Ok(line) => println!("{line}"),
40                    Err(receipt_error) => eprintln!(
41                        "u280-source-evidence-oracle: cannot serialize failure receipt: {receipt_error:#}"
42                    ),
43                }
44            }
45            eprintln!("u280-source-evidence-oracle: {error:#}");
46            ExitCode::FAILURE
47        }
48    }
49}
50
51fn receipt_line(outcome: &SourceOracleOutcome) -> Result<String> {
52    let result_directory = outcome
53        .result_directory
54        .to_str()
55        .context("result directory is not valid UTF-8")?;
56    serde_json::to_string(&serde_json::json!({
57        "schema": 2,
58        "kind": "hashsigs_u280_source_simulation_rust_oracle_receipt",
59        "result_kind": "completed_comparison",
60        "evidence_tier": "rhdl_source_simulation_plus_independent_rust_oracle",
61        "result_directory": result_directory,
62        "manifest": {
63            "file": "SHA256SUMS",
64            "bytes": outcome.result_manifest_bytes,
65            "sha256": outcome.result_manifest_sha256.as_str(),
66        },
67        "source_simulation_structurally_valid": true,
68        "cryptographic_oracle_valid": outcome.cryptographic_oracle_valid,
69        "source_revision_bound": false,
70        "source_worktree_state_bound": false,
71        "producing_command_bound": false,
72        "tool_versions_bound": false,
73        "promotion_blocked": true,
74        "hardware_execution": false,
75        "card": false,
76        "performance": false,
77        "hardware_completion_promotable": false,
78        "case": outcome.case.as_str(),
79        "batch": outcome.batch,
80        "compared_jobs": outcome.compared_jobs,
81        "all_unwritten_slots_valid": outcome.all_unwritten_slots_valid,
82    }))
83    .context("cannot serialize strict source-oracle receipt")
84}
85
86fn failure_receipt_line(failure: &SourceOracleFailure) -> Result<String> {
87    let result_directory = failure
88        .result_directory
89        .to_str()
90        .context("result directory is not valid UTF-8")?;
91    serde_json::to_string(&serde_json::json!({
92        "schema": 2,
93        "kind": "hashsigs_u280_source_simulation_rust_oracle_receipt",
94        "result_kind": "structural_failure",
95        "evidence_tier": "rhdl_source_simulation_plus_independent_rust_oracle",
96        "result_directory": result_directory,
97        "manifest": {
98            "file": "SHA256SUMS",
99            "bytes": failure.result_manifest_bytes,
100            "sha256": failure.result_manifest_sha256.as_str(),
101        },
102        "source_simulation_structurally_valid": false,
103        "cryptographic_oracle_valid": serde_json::Value::Null,
104        "source_revision_bound": false,
105        "source_worktree_state_bound": false,
106        "producing_command_bound": false,
107        "tool_versions_bound": false,
108        "promotion_blocked": true,
109        "hardware_execution": false,
110        "card": false,
111        "performance": false,
112        "hardware_completion_promotable": false,
113        "case": serde_json::Value::Null,
114        "batch": serde_json::Value::Null,
115        "compared_jobs": serde_json::Value::Null,
116        "all_unwritten_slots_valid": serde_json::Value::Null,
117    }))
118    .context("cannot serialize strict source-oracle failure receipt")
119}
120
121fn parse_options() -> Result<SourceOracleConfig> {
122    let mut arguments = env::args_os();
123    let _program = arguments.next();
124    let mut source_root = None;
125    let mut result_directory = None;
126    while let Some(argument) = arguments.next() {
127        match argument.to_str() {
128            Some("--source-root") => {
129                ensure!(source_root.is_none(), "--source-root was repeated");
130                source_root = Some(PathBuf::from(required_value(
131                    &mut arguments,
132                    "--source-root",
133                )?));
134            }
135            Some("--result-dir") => {
136                ensure!(result_directory.is_none(), "--result-dir was repeated");
137                result_directory = Some(PathBuf::from(required_value(
138                    &mut arguments,
139                    "--result-dir",
140                )?));
141            }
142            Some("--help" | "-h") => bail!(USAGE),
143            Some(value) => bail!("unknown option {value}; {USAGE}"),
144            None => bail!("option name is not valid UTF-8; {USAGE}"),
145        }
146    }
147    Ok(SourceOracleConfig {
148        source_root: source_root.with_context(|| USAGE.to_owned())?,
149        result_directory: result_directory.with_context(|| USAGE.to_owned())?,
150    })
151}
152
153fn required_value(
154    arguments: &mut impl Iterator<Item = OsString>,
155    option: &str,
156) -> Result<OsString> {
157    arguments
158        .next()
159        .with_context(|| format!("{option} requires a value; {USAGE}"))
160}
161
162#[cfg(test)]
163mod tests {
164    use super::{failure_receipt_line, receipt_line};
165    use std::path::PathBuf;
166    use u280_evidence_oracle::{SourceOracleFailure, SourceOracleOutcome};
167
168    #[test]
169    fn completed_receipt_preserves_the_source_only_boundary() {
170        let receipt = receipt_line(&SourceOracleOutcome {
171            cryptographic_oracle_valid: true,
172            case: "success_batch_1".to_owned(),
173            batch: 1,
174            compared_jobs: 1,
175            all_unwritten_slots_valid: true,
176            result_manifest_sha256: "ab".repeat(32),
177            result_manifest_bytes: 160,
178            result_directory: PathBuf::from("/tmp/source-result"),
179        })
180        .unwrap();
181        let value: serde_json::Value = serde_json::from_str(&receipt).unwrap();
182        assert_eq!(value["schema"].as_u64(), Some(2));
183        assert_eq!(value["cryptographic_oracle_valid"].as_bool(), Some(true));
184        assert_eq!(value["all_unwritten_slots_valid"].as_bool(), Some(true));
185        assert_eq!(value["hardware_execution"].as_bool(), Some(false));
186        assert_eq!(value["card"].as_bool(), Some(false));
187        assert_eq!(value["performance"].as_bool(), Some(false));
188        assert_eq!(value["source_revision_bound"].as_bool(), Some(false));
189        assert_eq!(value["promotion_blocked"].as_bool(), Some(true));
190        assert_eq!(
191            value["hardware_completion_promotable"].as_bool(),
192            Some(false)
193        );
194    }
195
196    #[test]
197    fn structural_failure_receipt_has_no_cryptographic_decision() {
198        let receipt = failure_receipt_line(&SourceOracleFailure {
199            result_directory: PathBuf::from("/tmp/source-failure"),
200            result_manifest_sha256: "cd".repeat(32),
201            result_manifest_bytes: 80,
202            failure: "malformed source archive".to_owned(),
203        })
204        .unwrap();
205        let value: serde_json::Value = serde_json::from_str(&receipt).unwrap();
206        assert_eq!(value["schema"].as_u64(), Some(2));
207        assert!(value["cryptographic_oracle_valid"].is_null());
208        assert!(value["all_unwritten_slots_valid"].is_null());
209        assert_eq!(value["hardware_execution"].as_bool(), Some(false));
210        assert_eq!(value["card"].as_bool(), Some(false));
211        assert_eq!(value["performance"].as_bool(), Some(false));
212        assert_eq!(value["source_revision_bound"].as_bool(), Some(false));
213        assert_eq!(value["promotion_blocked"].as_bool(), Some(true));
214    }
215}