1use std::collections::BTreeMap;
4use std::fs;
5use std::path::Path;
6
7use anyhow::{Context, Result, ensure};
8use serde::Serialize;
9use sha2::{Digest as _, Sha256};
10
11use crate::{CaseArtifacts, PROFILE_NAME, SimulationReport};
12
13#[derive(Serialize)]
14struct Geometry {
15 input_bytes_per_job: u64,
16 output_payload_bytes_per_job: u64,
17 output_slot_bytes_per_job: u64,
18 summary_bytes: u64,
19}
20
21#[derive(Serialize)]
22struct FileBinding {
23 file: &'static str,
24 bytes: u64,
25 sha256: String,
26}
27
28#[derive(Serialize)]
29struct OracleRequest<'a> {
30 schema: u64,
31 kind: &'static str,
32 evidence_tier: &'static str,
33 source_simulation_valid: bool,
34 cryptographic_oracle_valid: Option<bool>,
35 hardware_completion_promotable: bool,
36 independent_rust_oracle_required: bool,
37 profile: &'static str,
38 case: &'a str,
39 batch: u32,
40 geometry: Geometry,
41 successfully_retired_jobs: &'a [u32],
42 compare_all_jobs: bool,
43 files: Vec<FileBinding>,
44}
45
46fn sha256(bytes: &[u8]) -> String {
47 let digest = Sha256::digest(bytes);
48 let mut output = String::with_capacity(64);
49 for byte in digest {
50 use std::fmt::Write as _;
51 write!(&mut output, "{byte:02x}").expect("writing to a String cannot fail");
52 }
53 output
54}
55
56fn json_line<T: Serialize>(value: &T) -> Result<Vec<u8>> {
57 let mut bytes = serde_json::to_vec_pretty(value).context("cannot serialize simulation JSON")?;
58 bytes.push(b'\n');
59 Ok(bytes)
60}
61
62fn binding(file: &'static str, bytes: &[u8]) -> FileBinding {
63 FileBinding {
64 file,
65 bytes: u64::try_from(bytes.len()).expect("artifact length fits u64"),
66 sha256: sha256(bytes),
67 }
68}
69
70fn write_new_file(directory: &Path, name: &str, bytes: &[u8]) -> Result<()> {
71 let path = directory.join(name);
72 ensure!(
73 !path.exists(),
74 "refusing to replace artifact {}",
75 path.display()
76 );
77 fs::write(&path, bytes).with_context(|| format!("cannot write {}", path.display()))
78}
79
80pub fn write_case_artifacts(directory: &Path, artifacts: &CaseArtifacts) -> Result<()> {
87 ensure!(
88 !directory.exists(),
89 "refusing to replace source-simulation directory {}",
90 directory.display()
91 );
92 let batch = usize::try_from(artifacts.report.batch).context("batch does not fit this host")?;
93 let expected_inputs = batch.checked_mul(64).context("input geometry overflow")?;
94 let expected_outputs = batch
95 .max(1)
96 .checked_mul(4_096)
97 .context("output geometry overflow")?;
98 ensure!(
99 artifacts.inputs.len() == expected_inputs,
100 "source inputs differ from the request geometry"
101 );
102 ensure!(
103 artifacts.outputs.len() == expected_outputs,
104 "source outputs differ from the request geometry, including the zero-batch sentinel"
105 );
106 let report_bytes = json_line(&artifacts.report)?;
107 let request = OracleRequest {
108 schema: 1,
109 kind: "hashsigs_u280_source_simulation_oracle_request",
110 evidence_tier: "rhdl_source_simulation",
111 source_simulation_valid: true,
112 cryptographic_oracle_valid: None,
113 hardware_completion_promotable: false,
114 independent_rust_oracle_required: true,
115 profile: PROFILE_NAME,
116 case: &artifacts.report.case,
117 batch: artifacts.report.batch,
118 geometry: Geometry {
119 input_bytes_per_job: 64,
120 output_payload_bytes_per_job: 2_208,
121 output_slot_bytes_per_job: 4_096,
122 summary_bytes: 64,
123 },
124 successfully_retired_jobs: &artifacts.report.completed_jobs,
125 compare_all_jobs: artifacts.report.expected_success,
126 files: vec![
127 binding("inputs.bin", &artifacts.inputs),
128 binding("outputs.bin", &artifacts.outputs),
129 binding("summary.bin", &artifacts.summary),
130 binding("source-simulation.json", &report_bytes),
131 ],
132 };
133 let request_bytes = json_line(&request)?;
134 let files = [
135 ("inputs.bin", artifacts.inputs.as_slice()),
136 ("oracle-request.json", request_bytes.as_slice()),
137 ("outputs.bin", artifacts.outputs.as_slice()),
138 ("source-simulation.json", report_bytes.as_slice()),
139 ("summary.bin", artifacts.summary.as_slice()),
140 ];
141 fs::create_dir(directory)
142 .with_context(|| format!("cannot create simulation directory {}", directory.display()))?;
143 for (name, bytes) in files {
144 write_new_file(directory, name, bytes)?;
145 }
146 let mut manifest = String::new();
147 for (name, bytes) in files {
148 use std::fmt::Write as _;
149 writeln!(&mut manifest, "{} {name}", sha256(bytes))
150 .expect("writing to a String cannot fail");
151 }
152 write_new_file(directory, "SHA256SUMS", manifest.as_bytes())?;
153 Ok(())
154}
155
156pub fn write_suite_index(directory: &Path, reports: &[SimulationReport]) -> Result<()> {
162 #[derive(Serialize)]
163 struct Suite<'a> {
164 schema: u64,
165 kind: &'static str,
166 evidence_tier: &'static str,
167 source_simulation_valid: bool,
168 cryptographic_oracle_valid: Option<bool>,
169 hardware_completion_promotable: bool,
170 cases: &'a [SimulationReport],
171 }
172 let bytes = json_line(&Suite {
173 schema: 1,
174 kind: "hashsigs_u280_mock_hbm_source_simulation_suite",
175 evidence_tier: "rhdl_source_simulation",
176 source_simulation_valid: true,
177 cryptographic_oracle_valid: None,
178 hardware_completion_promotable: false,
179 cases: reports,
180 })?;
181 write_new_file(directory, "suite.json", &bytes)
182}
183
184pub(crate) fn merge_register_words(registers: &BTreeMap<String, u32>, prefix: &str) -> Result<u64> {
185 let low = registers
186 .get(&format!("{prefix}_lo"))
187 .copied()
188 .with_context(|| format!("missing {prefix}_lo register"))?;
189 let high = registers
190 .get(&format!("{prefix}_hi"))
191 .copied()
192 .with_context(|| format!("missing {prefix}_hi register"))?;
193 Ok(u64::from(low) | (u64::from(high) << 32))
194}