Skip to main content

u280_hbm_sim/
artifact.rs

1//! Source-simulation artifact serialization and independent-oracle handoff.
2
3use 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
80/// Write one completed source-simulation case to a new directory.
81///
82/// # Errors
83///
84/// Returns an error if the directory already exists, serialization fails, or
85/// any artifact cannot be written.
86pub 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
156/// Write a top-level index for a freshly created standard-suite directory.
157///
158/// # Errors
159///
160/// Returns an error if serialization or writing fails.
161pub 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}