Skip to main content

u280_evidence_oracle/
source.rs

1//! Independent oracle for the deliberately non-hardware mock-HBM source tier.
2
3use crate::archive::{
4    ArchiveSnapshot, FileRecord, ResultDirectory, canonical_transport_root, create_exclusive_file,
5    finish_writer, hex_lower, is_lower_hex, prepare_result_directory, read_verified_small,
6    seal_result_directory, sha256_bytes, verify_transport_archive, verify_transport_archive_again,
7    write_exclusive,
8};
9use crate::model::parse_strict_json;
10use anyhow::{Context, Result, ensure};
11use hashsigs_reference::{
12    FUSED_OUTPUT_BYTES, HashSigsSha256GenericV1, MessageDigest, PrivateSeed, SIGNATURE_BYTES,
13    Sha256GenericWots,
14};
15use serde::{Deserialize, Deserializer, Serialize};
16use sha2::{Digest as _, Sha256};
17use std::collections::{BTreeMap, BTreeSet, HashSet};
18use std::io::Write;
19use std::path::PathBuf;
20
21const PROFILE: &str = "HASHSIGS_SHA256_GENERIC_V1";
22const REQUEST_KIND: &str = "hashsigs_u280_source_simulation_oracle_request";
23const SIMULATION_KIND: &str = "hashsigs_u280_mock_hbm_source_simulation";
24const RESULT_KIND: &str = "hashsigs_u280_source_simulation_rust_oracle_result";
25const EVIDENCE_TIER: &str = "rhdl_source_simulation";
26const RESULT_EVIDENCE_TIER: &str = "rhdl_source_simulation_plus_independent_rust_oracle";
27const INPUT_BYTES_PER_JOB: usize = 64;
28const OUTPUT_PAYLOAD_BYTES_PER_JOB: usize = 2_208;
29const OUTPUT_SLOT_BYTES_PER_JOB: usize = 4_096;
30const OUTPUT_PADDING_BYTES_PER_JOB: usize = 1_888;
31const SUMMARY_BYTES: usize = 64;
32const INPUT_BYTES_PER_JOB_U64: u64 = 64;
33const OUTPUT_PAYLOAD_BYTES_PER_JOB_U64: u64 = 2_208;
34const OUTPUT_SLOT_BYTES_PER_JOB_U64: u64 = 4_096;
35const SUMMARY_BYTES_U64: u64 = 64;
36const MAXIMUM_MODELED_CYCLES: u64 = 60_000;
37const SHA_REQUESTS_PER_JOB: u64 = 1_258;
38const PAYLOAD_W_BEATS_PER_JOB: u32 = 35;
39const MAXIMUM_JSON_BYTES: u64 = 1024 * 1024;
40const MAXIMUM_ARTIFACT_BYTES: u64 = 6 * OUTPUT_SLOT_BYTES_PER_JOB_U64;
41const NONCE_DOMAIN: u64 = 0x4e4f_4e43_455f_5631;
42const WRITE_MODEL_DOMAIN: u64 = 0x5752_4954_455f_5631;
43const POISON_DOMAIN: u64 = 0x706f_6973_6f6e_7631;
44const BACKPRESSURE_MODEL: &str = "stateless_splitmix64_v1";
45
46const SOURCE_FILES: [&str; 5] = [
47    "inputs.bin",
48    "oracle-request.json",
49    "outputs.bin",
50    "source-simulation.json",
51    "summary.bin",
52];
53
54const _: () = {
55    assert!(FUSED_OUTPUT_BYTES == OUTPUT_PAYLOAD_BYTES_PER_JOB);
56    assert!(SIGNATURE_BYTES == 2_144);
57    assert!(
58        OUTPUT_SLOT_BYTES_PER_JOB - OUTPUT_PAYLOAD_BYTES_PER_JOB == OUTPUT_PADDING_BYTES_PER_JOB
59    );
60};
61
62/// Configuration for one immutable source-simulation comparison.
63#[derive(Clone, Debug)]
64pub struct SourceOracleConfig {
65    /// Directory containing one case's `SHA256SUMS` and five declared files.
66    pub source_root: PathBuf,
67    /// Brand-new directory in which the separate oracle result will be sealed.
68    pub result_directory: PathBuf,
69}
70
71/// Completed source-tier comparison and its externally anchorable manifest.
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct SourceOracleOutcome {
74    /// True only after every complete payload write and every required poison byte matched.
75    pub cryptographic_oracle_valid: bool,
76    /// Frozen source-simulation case name.
77    pub case: String,
78    /// Number of reserved job slots in the source case.
79    pub batch: u32,
80    /// Number of complete payload writes compared cryptographically.
81    ///
82    /// This can exceed the number of successfully retired jobs when a complete
83    /// payload write receives an error response.
84    pub compared_jobs: u32,
85    /// True only when every slot without a complete payload write retained poison.
86    pub all_unwritten_slots_valid: bool,
87    /// SHA-256 of the result directory's own `SHA256SUMS`.
88    pub result_manifest_sha256: String,
89    /// Exact byte count of the result directory's own `SHA256SUMS`.
90    pub result_manifest_bytes: u64,
91    /// Canonical path of the separately created result directory.
92    pub result_directory: PathBuf,
93}
94
95/// Structural source validation failure whose audit directory was sealed.
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct SourceOracleFailure {
98    /// Canonical path of the separately created failure directory.
99    pub result_directory: PathBuf,
100    /// SHA-256 of the failure directory's own `SHA256SUMS`.
101    pub result_manifest_sha256: String,
102    /// Exact byte count of the failure directory's own `SHA256SUMS`.
103    pub result_manifest_bytes: u64,
104    /// Original validation error rendered with its context chain.
105    pub failure: String,
106}
107
108impl std::fmt::Display for SourceOracleFailure {
109    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(
111            formatter,
112            "{}; source-oracle failure evidence sealed in {}",
113            self.failure,
114            self.result_directory.display()
115        )
116    }
117}
118
119impl std::error::Error for SourceOracleFailure {}
120
121fn required_option<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
122where
123    D: Deserializer<'de>,
124    T: Deserialize<'de>,
125{
126    Option::<T>::deserialize(deserializer)
127}
128
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
130#[serde(deny_unknown_fields)]
131struct Geometry {
132    input_bytes_per_job: u64,
133    output_payload_bytes_per_job: u64,
134    output_slot_bytes_per_job: u64,
135    summary_bytes: u64,
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
139#[serde(deny_unknown_fields)]
140struct SourceFileBinding {
141    file: String,
142    bytes: u64,
143    sha256: String,
144}
145
146#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
147#[serde(deny_unknown_fields)]
148// Flat booleans are fields in the frozen source-request wire schema.
149#[allow(clippy::struct_excessive_bools)]
150struct SourceOracleRequest {
151    schema: u64,
152    kind: String,
153    evidence_tier: String,
154    source_simulation_valid: bool,
155    #[serde(deserialize_with = "required_option")]
156    cryptographic_oracle_valid: Option<bool>,
157    hardware_completion_promotable: bool,
158    independent_rust_oracle_required: bool,
159    profile: String,
160    case: String,
161    batch: u32,
162    geometry: Geometry,
163    successfully_retired_jobs: Vec<u32>,
164    compare_all_jobs: bool,
165    files: Vec<SourceFileBinding>,
166}
167
168#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
169#[serde(deny_unknown_fields)]
170struct DecodedSummary {
171    terminal_code: u8,
172    nonce: u64,
173    payload_cycles: u64,
174    core_latency_cycles: u64,
175    capture_span_cycles: u64,
176    batch: u16,
177    input_ar: u16,
178    input_r: u16,
179    core_admissions: u16,
180    frames_captured: u16,
181    core_errors: u16,
182    payload_aw: u16,
183    payload_b: u16,
184    payload_w: u32,
185    memory_completed: u16,
186    error_flags: u16,
187}
188
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
190#[serde(deny_unknown_fields)]
191struct TerminalRegisters {
192    first_error_detail: u32,
193    first_error_job: u32,
194    last_status: u32,
195    memory_first_hi: u32,
196    memory_first_lo: u32,
197    memory_last_hi: u32,
198    memory_last_lo: u32,
199    memory_span_hi: u32,
200    memory_span_lo: u32,
201    output_stall_hi: u32,
202    output_stall_lo: u32,
203    progress0: u32,
204    progress1: u32,
205    queue_status: u32,
206}
207
208#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
209#[serde(deny_unknown_fields)]
210// Flat booleans are fields in the frozen source-report wire schema.
211#[allow(clippy::struct_excessive_bools)]
212struct SourceSimulationRecord {
213    schema: u64,
214    kind: String,
215    evidence_tier: String,
216    source_simulation_valid: bool,
217    #[serde(deserialize_with = "required_option")]
218    cryptographic_oracle_valid: Option<bool>,
219    hardware_completion_promotable: bool,
220    independent_rust_oracle_required: bool,
221    profile: String,
222    backpressure_model: String,
223    case: String,
224    seed: String,
225    batch: u32,
226    expected_success: bool,
227    maximum_modeled_cycles: u64,
228    modeled_cycles: u64,
229    sha_requests: u64,
230    payload_w_beats: u32,
231    completed_jobs: Vec<u32>,
232    summary: DecodedSummary,
233    summary_b_canonical: bool,
234    terminal_registers: TerminalRegisters,
235    memory_first: u64,
236    memory_last: u64,
237    memory_span: u64,
238}
239
240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
241enum Scenario {
242    SuccessBatch0,
243    SuccessBatch1,
244    SuccessBatch6,
245    SoftAbort,
246    InputError,
247    SignerError,
248    PayloadBError,
249    SummaryBError,
250}
251
252impl Scenario {
253    fn parse(name: &str) -> Result<Self> {
254        match name {
255            "success_batch_0" => Ok(Self::SuccessBatch0),
256            "success_batch_1" => Ok(Self::SuccessBatch1),
257            "success_batch_6" => Ok(Self::SuccessBatch6),
258            "soft_abort" => Ok(Self::SoftAbort),
259            "input_error" => Ok(Self::InputError),
260            "signer_error" => Ok(Self::SignerError),
261            "payload_b_error" => Ok(Self::PayloadBError),
262            "summary_b_error" => Ok(Self::SummaryBError),
263            _ => anyhow::bail!("source-simulation case is not in the frozen inventory"),
264        }
265    }
266
267    const fn batch(self) -> u32 {
268        match self {
269            Self::SuccessBatch0 => 0,
270            Self::SuccessBatch6 | Self::SoftAbort => 6,
271            _ => 1,
272        }
273    }
274
275    const fn expected_success(self) -> bool {
276        matches!(
277            self,
278            Self::SuccessBatch0 | Self::SuccessBatch1 | Self::SuccessBatch6
279        )
280    }
281
282    const fn terminal(self) -> (u8, u16, bool) {
283        match self {
284            Self::SuccessBatch0 | Self::SuccessBatch1 | Self::SuccessBatch6 => (0, 0, true),
285            Self::SoftAbort => (2, 1 << 7, true),
286            Self::InputError => (3, 1 << 1, true),
287            Self::SignerError => (5, 1 << 3, true),
288            Self::PayloadBError => (4, 1 << 2, true),
289            Self::SummaryBError => (7, 1 << 9, false),
290        }
291    }
292
293    const fn written_payload_jobs(self) -> u32 {
294        match self {
295            Self::SuccessBatch0 | Self::SoftAbort | Self::InputError | Self::SignerError => 0,
296            Self::SuccessBatch1 | Self::PayloadBError | Self::SummaryBError => 1,
297            Self::SuccessBatch6 => 6,
298        }
299    }
300}
301
302#[derive(Clone, Debug, Serialize)]
303struct ManifestBinding {
304    file: &'static str,
305    bytes: u64,
306    sha256: String,
307}
308
309#[derive(Clone, Debug, Serialize)]
310struct JobTranscript {
311    schema: u64,
312    job_index: u32,
313    input_sha256: String,
314    expected_sha256: String,
315    observed_sha256: String,
316    payload_match: bool,
317    padding_match: bool,
318}
319
320#[derive(Clone, Debug, Serialize)]
321// Flat booleans preserve the published success-evidence JSON schema.
322#[allow(clippy::struct_excessive_bools)]
323struct SuccessSummary {
324    schema: u64,
325    kind: &'static str,
326    evidence_tier: &'static str,
327    producer: &'static str,
328    pinned_hashsigs_rs_commit: &'static str,
329    profile: &'static str,
330    source_manifest: ManifestBinding,
331    source_request_sha256: String,
332    case: String,
333    seed: String,
334    batch: u32,
335    successfully_retired_jobs: Vec<u32>,
336    compared_jobs: u32,
337    matched_jobs: u32,
338    mismatched_jobs: u32,
339    regenerated_inputs_match: bool,
340    all_reserved_padding_valid: bool,
341    all_unwritten_slots_valid: bool,
342    source_simulation_valid: bool,
343    cryptographic_oracle_valid: bool,
344    source_revision_bound: bool,
345    source_worktree_state_bound: bool,
346    producing_command_bound: bool,
347    tool_versions_bound: bool,
348    promotion_blocked: bool,
349    hardware_execution: bool,
350    card: bool,
351    performance: bool,
352    hardware_completion_promotable: bool,
353    evidence_boundary: &'static str,
354}
355
356#[derive(Clone, Debug, Serialize)]
357// Flat booleans preserve the published failure-evidence JSON schema.
358#[allow(clippy::struct_excessive_bools)]
359struct FailureSummary {
360    schema: u64,
361    kind: &'static str,
362    evidence_tier: &'static str,
363    source_manifest: Option<ManifestBinding>,
364    source_simulation_valid: Option<bool>,
365    cryptographic_oracle_valid: Option<bool>,
366    all_unwritten_slots_valid: Option<bool>,
367    source_revision_bound: bool,
368    source_worktree_state_bound: bool,
369    producing_command_bound: bool,
370    tool_versions_bound: bool,
371    promotion_blocked: bool,
372    hardware_execution: bool,
373    card: bool,
374    performance: bool,
375    hardware_completion_promotable: bool,
376    failure: String,
377}
378
379struct ValidationOutput {
380    summary: SuccessSummary,
381    jobs_record: FileRecord,
382}
383
384#[derive(Clone, Copy)]
385struct ValidatedSourceCase {
386    seed: u64,
387    written_payload_jobs: u32,
388}
389
390struct OutputComparison {
391    jobs_record: FileRecord,
392    compared_jobs: u32,
393    matched_jobs: u32,
394    all_padding_valid: bool,
395    all_unwritten_slots_valid: bool,
396}
397
398/// Validate one sealed source-simulation case and create a separate sealed result.
399///
400/// Structural failures leave an anchorable `failure.json` when the new result
401/// directory can be created. A completed payload or poison mismatch emits a
402/// normal result with `cryptographic_oracle_valid:false`.
403///
404/// # Errors
405///
406/// Returns an error for any malformed, confused, incomplete, mutated, or
407/// concurrently replaced source archive, or for result-writing failures.
408pub fn execute_source(config: &SourceOracleConfig) -> Result<SourceOracleOutcome> {
409    let source_root = canonical_transport_root(&config.source_root)?;
410    let result_directory = prepare_result_directory(&source_root, &config.result_directory)?;
411    let mut source_manifest = None;
412    let operation: Result<SourceOracleOutcome> = (|| {
413        let snapshot = verify_transport_archive(&source_root)?;
414        source_manifest = Some(manifest_binding(&snapshot.manifest));
415        let output = validate_source_archive(&snapshot, &result_directory)?;
416        let valid = output.summary.cryptographic_oracle_valid;
417        let case = output.summary.case.clone();
418        let batch = output.summary.batch;
419        let compared_jobs = output.summary.compared_jobs;
420        let all_unwritten_slots_valid = output.summary.all_unwritten_slots_valid;
421        let summary_bytes = canonical_json_line(&output.summary)?;
422        let summary_record = write_exclusive(&result_directory, "summary.json", &summary_bytes)?;
423        let expected = BTreeMap::from([
424            ("jobs.jsonl".to_owned(), output.jobs_record),
425            ("summary.json".to_owned(), summary_record),
426        ]);
427        let result_manifest = seal_result_directory(&result_directory, &expected)?;
428        Ok(SourceOracleOutcome {
429            cryptographic_oracle_valid: valid,
430            case,
431            batch,
432            compared_jobs,
433            all_unwritten_slots_valid,
434            result_manifest_sha256: result_manifest.sha256,
435            result_manifest_bytes: result_manifest.bytes,
436            result_directory: result_directory.path.clone(),
437        })
438    })();
439
440    match operation {
441        Ok(outcome) => Ok(outcome),
442        Err(error) => {
443            for name in ["SHA256SUMS", "jobs.jsonl", "summary.json"] {
444                if let Err(cleanup_error) = result_directory.remove_if_exists(name) {
445                    return Err(error.context(format!(
446                        "also failed to remove partial source-oracle result {name}: {cleanup_error:#}"
447                    )));
448                }
449            }
450            let failure = FailureSummary {
451                schema: 2,
452                kind: "hashsigs_u280_source_simulation_rust_oracle_failure",
453                evidence_tier: RESULT_EVIDENCE_TIER,
454                source_manifest,
455                source_simulation_valid: None,
456                cryptographic_oracle_valid: None,
457                all_unwritten_slots_valid: None,
458                source_revision_bound: false,
459                source_worktree_state_bound: false,
460                producing_command_bound: false,
461                tool_versions_bound: false,
462                promotion_blocked: true,
463                hardware_execution: false,
464                card: false,
465                performance: false,
466                hardware_completion_promotable: false,
467                failure: format!("{error:#}"),
468            };
469            let write_result = canonical_json_line(&failure).and_then(|bytes| {
470                let record = write_exclusive(&result_directory, "failure.json", &bytes)?;
471                seal_result_directory(
472                    &result_directory,
473                    &BTreeMap::from([("failure.json".to_owned(), record)]),
474                )
475            });
476            let result_manifest = match write_result {
477                Ok(record) => record,
478                Err(evidence_error) => {
479                    return Err(error.context(format!(
480                        "also failed to seal source-oracle failure evidence: {evidence_error:#}"
481                    )));
482                }
483            };
484            Err(SourceOracleFailure {
485                result_directory: result_directory.path.clone(),
486                result_manifest_sha256: result_manifest.sha256,
487                result_manifest_bytes: result_manifest.bytes,
488                failure: format!("{error:#}"),
489            }
490            .into())
491        }
492    }
493}
494
495fn validate_source_archive(
496    snapshot: &ArchiveSnapshot,
497    result_directory: &ResultDirectory,
498) -> Result<ValidationOutput> {
499    validate_exact_layout(snapshot)?;
500    let request_bytes = read_verified_small(snapshot, "oracle-request.json", MAXIMUM_JSON_BYTES)?;
501    let request: SourceOracleRequest = parse_strict_json(&request_bytes, "oracle-request.json")?;
502    let report_bytes = read_verified_small(snapshot, "source-simulation.json", MAXIMUM_JSON_BYTES)?;
503    let report: SourceSimulationRecord =
504        parse_strict_json(&report_bytes, "source-simulation.json")?;
505    let validated = validate_request_and_report(snapshot, &request, &report)?;
506
507    let input_bytes = read_verified_small(snapshot, "inputs.bin", MAXIMUM_ARTIFACT_BYTES)?;
508    let output_bytes = read_verified_small(snapshot, "outputs.bin", MAXIMUM_ARTIFACT_BYTES)?;
509    let summary_bytes = read_verified_small(snapshot, "summary.bin", SUMMARY_BYTES_U64)?;
510    validate_binary_geometry(&request, &input_bytes, &output_bytes, &summary_bytes)?;
511    validate_regenerated_inputs(validated.seed, request.batch, &input_bytes)?;
512    validate_summary_bytes(&report, validated.seed, &summary_bytes)?;
513    let comparison = compare_output_slots(
514        result_directory,
515        &input_bytes,
516        &output_bytes,
517        request.batch,
518        validated,
519    )?;
520
521    let after = verify_transport_archive_again(snapshot)?;
522    ensure!(
523        &after == snapshot,
524        "source-simulation archive changed during oracle execution"
525    );
526    build_validation_output(snapshot, &request_bytes, request, report, comparison)
527}
528
529fn compare_output_slots(
530    result_directory: &ResultDirectory,
531    inputs: &[u8],
532    outputs: &[u8],
533    batch: u32,
534    validated: ValidatedSourceCase,
535) -> Result<OutputComparison> {
536    ensure!(
537        validated.written_payload_jobs <= batch,
538        "complete payload writes exceed reserved output slots"
539    );
540    let mut jobs_writer = create_exclusive_file(result_directory, "jobs.jsonl")?;
541    let mut matched_jobs = 0_u32;
542    let mut all_padding_valid = true;
543    let mut all_unwritten_slots_valid = true;
544    let oracle = Sha256GenericWots::new();
545    for job in 0..batch.max(1) {
546        let output_offset = job_offset(job, OUTPUT_SLOT_BYTES_PER_JOB, "output")?;
547        let slot = &outputs[output_offset..output_offset + OUTPUT_SLOT_BYTES_PER_JOB];
548        let padding_offset = output_offset
549            .checked_add(OUTPUT_PAYLOAD_BYTES_PER_JOB)
550            .context("padding offset overflow")?;
551        let padding_match = poison_matches(
552            validated.seed,
553            padding_offset,
554            &slot[OUTPUT_PAYLOAD_BYTES_PER_JOB..],
555        );
556        all_padding_valid &= padding_match;
557        if job < validated.written_payload_jobs {
558            let input_offset = job_offset(job, INPUT_BYTES_PER_JOB, "input")?;
559            let input = &inputs[input_offset..input_offset + INPUT_BYTES_PER_JOB];
560            let payload_match = write_payload_comparison(
561                &mut jobs_writer,
562                oracle,
563                job,
564                input,
565                slot,
566                padding_match,
567            )?;
568            if payload_match {
569                matched_jobs = matched_jobs
570                    .checked_add(1)
571                    .context("matched-job counter overflow")?;
572            }
573        } else {
574            all_unwritten_slots_valid &= poison_matches(validated.seed, output_offset, slot);
575        }
576    }
577    let jobs_record = finish_writer(jobs_writer)?;
578    result_directory.sync()?;
579    Ok(OutputComparison {
580        jobs_record,
581        compared_jobs: validated.written_payload_jobs,
582        matched_jobs,
583        all_padding_valid,
584        all_unwritten_slots_valid,
585    })
586}
587
588fn write_payload_comparison(
589    writer: &mut impl Write,
590    oracle: Sha256GenericWots,
591    job: u32,
592    input: &[u8],
593    slot: &[u8],
594    padding_match: bool,
595) -> Result<bool> {
596    let expected = expected_payload(oracle, input)?;
597    let observed = &slot[..OUTPUT_PAYLOAD_BYTES_PER_JOB];
598    let payload_match = observed == expected.as_slice();
599    let transcript = JobTranscript {
600        schema: 1,
601        job_index: job,
602        input_sha256: hex_lower(&Sha256::digest(input)),
603        expected_sha256: hex_lower(&Sha256::digest(expected)),
604        observed_sha256: hex_lower(&Sha256::digest(observed)),
605        payload_match,
606        padding_match,
607    };
608    serde_json::to_writer(&mut *writer, &transcript)
609        .context("cannot serialize source-oracle job transcript")?;
610    writer
611        .write_all(b"\n")
612        .context("cannot write source-oracle job transcript")?;
613    Ok(payload_match)
614}
615
616fn job_offset(job: u32, stride: usize, label: &str) -> Result<usize> {
617    usize::try_from(job)
618        .context("job index does not fit this host")?
619        .checked_mul(stride)
620        .with_context(|| format!("{label} offset overflow"))
621}
622
623fn poison_matches(seed: u64, start_offset: usize, bytes: &[u8]) -> bool {
624    bytes.iter().enumerate().all(|(relative_offset, byte)| {
625        start_offset
626            .checked_add(relative_offset)
627            .is_some_and(|offset| *byte == source_poison(seed, offset))
628    })
629}
630
631fn build_validation_output(
632    snapshot: &ArchiveSnapshot,
633    request_bytes: &[u8],
634    request: SourceOracleRequest,
635    report: SourceSimulationRecord,
636    comparison: OutputComparison,
637) -> Result<ValidationOutput> {
638    let mismatched_jobs = comparison
639        .compared_jobs
640        .checked_sub(comparison.matched_jobs)
641        .context("mismatched-job counter underflow")?;
642    let cryptographic_oracle_valid = mismatched_jobs == 0
643        && comparison.all_padding_valid
644        && comparison.all_unwritten_slots_valid;
645    Ok(ValidationOutput {
646        jobs_record: comparison.jobs_record,
647        summary: SuccessSummary {
648            schema: 2,
649            kind: RESULT_KIND,
650            evidence_tier: RESULT_EVIDENCE_TIER,
651            producer: "independent Rust Sha256GenericWots source-simulation oracle",
652            pinned_hashsigs_rs_commit: "2d315dd4168804b7cbc51c51a1bf7ca27bf74140",
653            profile: PROFILE,
654            source_manifest: manifest_binding(&snapshot.manifest),
655            source_request_sha256: sha256_bytes(request_bytes),
656            case: request.case,
657            seed: report.seed,
658            batch: request.batch,
659            successfully_retired_jobs: request.successfully_retired_jobs,
660            compared_jobs: comparison.compared_jobs,
661            matched_jobs: comparison.matched_jobs,
662            mismatched_jobs,
663            regenerated_inputs_match: true,
664            all_reserved_padding_valid: comparison.all_padding_valid,
665            all_unwritten_slots_valid: comparison.all_unwritten_slots_valid,
666            source_simulation_valid: true,
667            cryptographic_oracle_valid,
668            source_revision_bound: false,
669            source_worktree_state_bound: false,
670            producing_command_bound: false,
671            tool_versions_bound: false,
672            promotion_blocked: true,
673            hardware_execution: false,
674            card: false,
675            performance: false,
676            hardware_completion_promotable: false,
677            evidence_boundary: "source-tier RHDL simulation complete-written-payload and all-nonwritten-slot poison comparison only; retirement remains a separate transport classification, the producer format does not bind source revision, dirty state, producing command, or tool versions, and no generated-RTL equivalence, synthesis, implementation, timing, performance, XRT, card, or hardware execution was established",
678        },
679    })
680}
681
682fn validate_exact_layout(snapshot: &ArchiveSnapshot) -> Result<()> {
683    let expected = SOURCE_FILES
684        .iter()
685        .map(|name| (*name).to_owned())
686        .collect::<BTreeSet<_>>();
687    ensure!(
688        snapshot.files.keys().cloned().collect::<BTreeSet<_>>() == expected,
689        "source-simulation archive has missing or extra files"
690    );
691    ensure!(
692        snapshot.directories.is_empty(),
693        "source-simulation archive contains an unexpected directory"
694    );
695    Ok(())
696}
697
698fn validate_request_and_report(
699    snapshot: &ArchiveSnapshot,
700    request: &SourceOracleRequest,
701    report: &SourceSimulationRecord,
702) -> Result<ValidatedSourceCase> {
703    validate_request_header(request)?;
704    validate_report_header(report)?;
705    ensure!(
706        request.case == report.case,
707        "request and source report case differ"
708    );
709    ensure!(
710        request.batch == report.batch,
711        "request and source report batch differ"
712    );
713    ensure!(
714        request.successfully_retired_jobs == report.completed_jobs,
715        "request and source report retired-job lists differ"
716    );
717    ensure!(
718        request.compare_all_jobs == report.expected_success,
719        "request comparison scope differs from the source result"
720    );
721    let scenario = Scenario::parse(&request.case)?;
722    ensure!(
723        request.batch == scenario.batch(),
724        "case has the wrong frozen batch"
725    );
726    ensure!(
727        report.expected_success == scenario.expected_success(),
728        "case success classification changed"
729    );
730    validate_job_list(
731        &request.successfully_retired_jobs,
732        request.batch,
733        scenario.expected_success(),
734    )?;
735    validate_file_bindings(snapshot, request)?;
736    let seed = parse_seed(&report.seed)?;
737    validate_scenario_record(report, scenario, seed)?;
738    // The frozen shell writes payloads in ascending job order. Scenario
739    // validation above binds every successful prefix and both one-frame error
740    // cases to their exact AW/W/B and first-error/completion records.
741    Ok(ValidatedSourceCase {
742        seed,
743        written_payload_jobs: u32::from(report.summary.payload_aw),
744    })
745}
746
747fn validate_request_header(request: &SourceOracleRequest) -> Result<()> {
748    ensure!(
749        request.schema == 1,
750        "source oracle-request schema must be 1"
751    );
752    ensure!(
753        request.kind == REQUEST_KIND,
754        "archive is not a source-simulation oracle request"
755    );
756    ensure!(
757        request.evidence_tier == EVIDENCE_TIER,
758        "request evidence tier changed"
759    );
760    ensure!(
761        request.source_simulation_valid,
762        "request did not pass source simulation"
763    );
764    ensure!(
765        request.cryptographic_oracle_valid.is_none(),
766        "request pre-populated the oracle decision"
767    );
768    ensure!(
769        !request.hardware_completion_promotable,
770        "request contains a hardware-like completion claim"
771    );
772    ensure!(
773        request.independent_rust_oracle_required,
774        "request does not require an independent Rust oracle"
775    );
776    ensure!(
777        request.profile == PROFILE,
778        "request cryptographic profile changed"
779    );
780    ensure!(
781        request.geometry
782            == (Geometry {
783                input_bytes_per_job: INPUT_BYTES_PER_JOB_U64,
784                output_payload_bytes_per_job: OUTPUT_PAYLOAD_BYTES_PER_JOB_U64,
785                output_slot_bytes_per_job: OUTPUT_SLOT_BYTES_PER_JOB_U64,
786                summary_bytes: SUMMARY_BYTES_U64,
787            }),
788        "request geometry changed"
789    );
790    Ok(())
791}
792
793fn validate_report_header(report: &SourceSimulationRecord) -> Result<()> {
794    ensure!(report.schema == 1, "source-simulation schema must be 1");
795    ensure!(
796        report.kind == SIMULATION_KIND,
797        "archive is not the mock-HBM source simulator"
798    );
799    ensure!(
800        report.evidence_tier == EVIDENCE_TIER,
801        "source report evidence tier changed"
802    );
803    ensure!(
804        report.source_simulation_valid,
805        "source report is not locally valid"
806    );
807    ensure!(
808        report.cryptographic_oracle_valid.is_none(),
809        "source report pre-populated the oracle decision"
810    );
811    ensure!(
812        !report.hardware_completion_promotable,
813        "source report contains a hardware-like completion claim"
814    );
815    ensure!(
816        report.independent_rust_oracle_required,
817        "source report waived the independent Rust oracle"
818    );
819    ensure!(
820        report.profile == PROFILE,
821        "source report cryptographic profile changed"
822    );
823    ensure!(
824        report.backpressure_model == BACKPRESSURE_MODEL,
825        "source backpressure model changed"
826    );
827    ensure!(
828        report.maximum_modeled_cycles == MAXIMUM_MODELED_CYCLES
829            && (1..=MAXIMUM_MODELED_CYCLES).contains(&report.modeled_cycles),
830        "source report cycle bound changed or was not met"
831    );
832    Ok(())
833}
834
835fn validate_file_bindings(snapshot: &ArchiveSnapshot, request: &SourceOracleRequest) -> Result<()> {
836    const BOUND_FILES: [&str; 4] = [
837        "inputs.bin",
838        "outputs.bin",
839        "summary.bin",
840        "source-simulation.json",
841    ];
842    ensure!(
843        request.files.len() == BOUND_FILES.len(),
844        "request file binding count changed"
845    );
846    for (binding, expected_name) in request.files.iter().zip(BOUND_FILES) {
847        ensure!(
848            binding.file == expected_name,
849            "request file bindings are missing, duplicated, or out of order"
850        );
851        ensure!(
852            is_lower_hex(&binding.sha256, 64),
853            "request file digest is not canonical SHA-256"
854        );
855        let sealed = snapshot
856            .files
857            .get(expected_name)
858            .with_context(|| format!("manifest omits {expected_name}"))?;
859        ensure!(
860            binding.bytes == sealed.bytes && binding.sha256 == sealed.sha256,
861            "request binding differs from sealed {expected_name}"
862        );
863    }
864    Ok(())
865}
866
867fn validate_job_list(jobs: &[u32], batch: u32, require_all: bool) -> Result<()> {
868    let mut previous = None;
869    for &job in jobs {
870        ensure!(job < batch, "retired job is outside the reserved batch");
871        if let Some(prior) = previous {
872            ensure!(prior < job, "retired jobs are duplicated or out of order");
873        }
874        previous = Some(job);
875    }
876    if require_all {
877        ensure!(
878            jobs == (0..batch).collect::<Vec<_>>(),
879            "successful source case does not compare every job in order"
880        );
881    }
882    Ok(())
883}
884
885fn validate_scenario_record(
886    report: &SourceSimulationRecord,
887    scenario: Scenario,
888    seed: u64,
889) -> Result<()> {
890    validate_scenario_summary(report, scenario, seed)?;
891    validate_terminal_record(report, scenario)?;
892    validate_counter_relationships(report, scenario)?;
893    validate_capture_timing(report)?;
894    validate_memory_timing(report)?;
895    ensure!(
896        u32::from(report.summary.payload_aw) == scenario.written_payload_jobs(),
897        "complete payload-write count differs from the frozen case"
898    );
899    if scenario.expected_success() {
900        validate_success_progress(report)
901    } else {
902        validate_failure_progress(report, scenario)
903    }
904}
905
906fn validate_scenario_summary(
907    report: &SourceSimulationRecord,
908    scenario: Scenario,
909    seed: u64,
910) -> Result<()> {
911    let (terminal, error_flags, canonical_summary_b) = scenario.terminal();
912    ensure!(
913        report.summary_b_canonical == canonical_summary_b,
914        "summary-B classification differs from the frozen case"
915    );
916    ensure!(
917        report.summary.nonce == seed ^ NONCE_DOMAIN,
918        "summary nonce differs from the deterministic launch nonce"
919    );
920    ensure!(
921        u32::from(report.summary.batch) == report.batch,
922        "summary batch differs from the report"
923    );
924    ensure!(
925        report.payload_w_beats == report.summary.payload_w,
926        "payload-W counters differ"
927    );
928    ensure!(
929        usize::from(report.summary.memory_completed) == report.completed_jobs.len(),
930        "memory completion count differs from retired jobs"
931    );
932    ensure!(
933        report.summary.payload_cycles <= report.modeled_cycles
934            && report.summary.core_latency_cycles <= report.summary.payload_cycles
935            && report.summary.capture_span_cycles <= report.summary.core_latency_cycles,
936        "summary timing counters exceed their enclosing source-simulation intervals"
937    );
938
939    let (summary_terminal, summary_flags) = if scenario == Scenario::SummaryBError {
940        (0, 0)
941    } else {
942        (terminal, error_flags)
943    };
944    ensure!(
945        report.summary.terminal_code == summary_terminal,
946        "summary terminal code differs from the frozen case"
947    );
948    ensure!(
949        report.summary.error_flags == summary_flags,
950        "summary error flags differ from the frozen case"
951    );
952    Ok(())
953}
954
955fn validate_terminal_record(report: &SourceSimulationRecord, scenario: Scenario) -> Result<()> {
956    let (terminal, error_flags, canonical_summary_b) = scenario.terminal();
957    let expected_occurrence = if canonical_summary_b { 0x0f } else { 0x07 };
958    let expected_last_status =
959        u32::from(terminal) | (u32::from(error_flags) << 8) | (expected_occurrence << 24);
960    ensure!(
961        report.terminal_registers.last_status == expected_last_status,
962        "LAST_STATUS differs from the frozen terminal contract"
963    );
964    ensure!(
965        report.terminal_registers.progress0
966            == u32::from(report.summary.core_admissions)
967                | (u32::from(report.summary.frames_captured) << 16),
968        "PROGRESS0 differs from the decoded summary"
969    );
970    ensure!(
971        report.terminal_registers.progress1
972            == u32::from(report.summary.memory_completed)
973                | (u32::from(report.summary.core_errors) << 16),
974        "PROGRESS1 differs from the decoded summary"
975    );
976    ensure!(
977        merge_words(
978            report.terminal_registers.memory_first_lo,
979            report.terminal_registers.memory_first_hi
980        ) == report.memory_first,
981        "memory-first register binding changed"
982    );
983    ensure!(
984        merge_words(
985            report.terminal_registers.memory_last_lo,
986            report.terminal_registers.memory_last_hi
987        ) == report.memory_last,
988        "memory-last register binding changed"
989    );
990    ensure!(
991        merge_words(
992            report.terminal_registers.memory_span_lo,
993            report.terminal_registers.memory_span_hi
994        ) == report.memory_span,
995        "memory-span register binding changed"
996    );
997    ensure!(
998        report.terminal_registers.queue_status == 0,
999        "terminal queue diagnostics are not closed"
1000    );
1001    ensure!(
1002        merge_words(
1003            report.terminal_registers.output_stall_lo,
1004            report.terminal_registers.output_stall_hi,
1005        ) <= report.summary.payload_cycles,
1006        "output-stall counter exceeds the execute/abort interval"
1007    );
1008    ensure!(
1009        report.terminal_registers.first_error_job == u32::MAX
1010            || report.terminal_registers.first_error_job < report.batch,
1011        "first-error job is outside the frozen batch"
1012    );
1013    Ok(())
1014}
1015
1016fn validate_success_progress(report: &SourceSimulationRecord) -> Result<()> {
1017    ensure!(
1018        report.terminal_registers.first_error_job == u32::MAX
1019            && report.terminal_registers.first_error_detail == 0,
1020        "successful source case contains first-error diagnostics"
1021    );
1022    ensure!(
1023        report.sha_requests == u64::from(report.batch) * SHA_REQUESTS_PER_JOB,
1024        "successful SHA request count changed"
1025    );
1026    ensure!(
1027        report.payload_w_beats == report.batch * PAYLOAD_W_BEATS_PER_JOB,
1028        "successful payload-W count changed"
1029    );
1030    ensure!(
1031        report.summary.input_ar == report.summary.batch
1032            && report.summary.input_r == report.summary.batch
1033            && report.summary.core_admissions == report.summary.batch
1034            && report.summary.frames_captured == report.summary.batch
1035            && report.summary.payload_aw == report.summary.batch
1036            && report.summary.payload_b == report.summary.batch,
1037        "successful source progress changed"
1038    );
1039    ensure!(
1040        report.summary.core_errors == 0,
1041        "successful source case contains a signer error"
1042    );
1043    Ok(())
1044}
1045
1046fn validate_failure_progress(report: &SourceSimulationRecord, scenario: Scenario) -> Result<()> {
1047    match scenario {
1048        Scenario::SoftAbort => validate_soft_abort_progress(report),
1049        Scenario::InputError => validate_input_error_progress(report),
1050        Scenario::SignerError => validate_signer_error_progress(report),
1051        Scenario::PayloadBError => validate_payload_b_error_progress(report),
1052        Scenario::SummaryBError => validate_summary_b_error_progress(report),
1053        Scenario::SuccessBatch0 | Scenario::SuccessBatch1 | Scenario::SuccessBatch6 => {
1054            anyhow::bail!("internal source-scenario classification mismatch")
1055        }
1056    }
1057}
1058
1059fn validate_soft_abort_progress(report: &SourceSimulationRecord) -> Result<()> {
1060    ensure!(
1061        (1..SHA_REQUESTS_PER_JOB).contains(&report.sha_requests),
1062        "soft abort signer work changed"
1063    );
1064    ensure!(
1065        report.summary.core_admissions > 0
1066            && report.summary.frames_captured == 0
1067            && report.summary.core_errors == 0
1068            && report.summary.payload_aw == 0
1069            && report.summary.payload_w == 0
1070            && report.summary.payload_b == 0
1071            && report.summary.memory_completed == 0,
1072        "soft abort progress changed"
1073    );
1074    ensure!(
1075        report.terminal_registers.first_error_job == u32::MAX
1076            && report.terminal_registers.first_error_detail == 0x4001,
1077        "soft abort diagnostics changed"
1078    );
1079    Ok(())
1080}
1081
1082fn validate_input_error_progress(report: &SourceSimulationRecord) -> Result<()> {
1083    ensure!(
1084        report.sha_requests == 0
1085            && report.summary.input_ar == 1
1086            && report.summary.input_r == 0
1087            && report.summary.core_admissions == 0
1088            && report.summary.frames_captured == 0
1089            && report.summary.core_errors == 0
1090            && report.summary.payload_aw == 0
1091            && report.summary.payload_w == 0
1092            && report.summary.payload_b == 0
1093            && report.summary.memory_completed == 0,
1094        "input-error progress changed"
1095    );
1096    ensure!(
1097        report.terminal_registers.first_error_job == 0
1098            && report.terminal_registers.first_error_detail == 0x0202,
1099        "input-error diagnostics changed"
1100    );
1101    Ok(())
1102}
1103
1104fn validate_signer_error_progress(report: &SourceSimulationRecord) -> Result<()> {
1105    ensure!(
1106        (1..SHA_REQUESTS_PER_JOB).contains(&report.sha_requests),
1107        "signer-error work changed"
1108    );
1109    ensure!(
1110        report.summary.core_admissions == 1
1111            && report.summary.frames_captured == 0
1112            && report.summary.core_errors == 1
1113            && report.summary.payload_aw == 0
1114            && report.summary.payload_w == 0
1115            && report.summary.payload_b == 0
1116            && report.summary.memory_completed == 0,
1117        "signer-error progress changed"
1118    );
1119    ensure!(
1120        report.terminal_registers.first_error_job == 0
1121            && matches!(
1122                report.terminal_registers.first_error_detail,
1123                0 | 0x0100 | 0x0200
1124            ),
1125        "signer-error diagnostics changed"
1126    );
1127    Ok(())
1128}
1129
1130fn validate_payload_b_error_progress(report: &SourceSimulationRecord) -> Result<()> {
1131    validate_single_written_payload_progress(report, 0)?;
1132    ensure!(
1133        report.terminal_registers.first_error_job == 0
1134            && report.terminal_registers.first_error_detail == 2,
1135        "payload-B error diagnostics changed"
1136    );
1137    Ok(())
1138}
1139
1140fn validate_summary_b_error_progress(report: &SourceSimulationRecord) -> Result<()> {
1141    validate_single_written_payload_progress(report, 1)?;
1142    ensure!(
1143        report.terminal_registers.first_error_job == u32::MAX
1144            && report.terminal_registers.first_error_detail == 2,
1145        "summary-B error diagnostics changed"
1146    );
1147    Ok(())
1148}
1149
1150fn validate_single_written_payload_progress(
1151    report: &SourceSimulationRecord,
1152    memory_completed: u16,
1153) -> Result<()> {
1154    ensure!(
1155        report.sha_requests == SHA_REQUESTS_PER_JOB
1156            && report.summary.input_ar == 1
1157            && report.summary.input_r == 1
1158            && report.summary.core_admissions == 1
1159            && report.summary.frames_captured == 1
1160            && report.summary.core_errors == 0
1161            && report.summary.payload_aw == 1
1162            && report.summary.payload_w == PAYLOAD_W_BEATS_PER_JOB
1163            && report.summary.payload_b == 1
1164            && report.summary.memory_completed == memory_completed,
1165        "single-payload failure progress changed"
1166    );
1167    Ok(())
1168}
1169
1170fn validate_counter_relationships(
1171    report: &SourceSimulationRecord,
1172    scenario: Scenario,
1173) -> Result<()> {
1174    let batch = u32::from(report.summary.batch);
1175    for (label, value) in [
1176        ("input AR", report.summary.input_ar),
1177        ("input R", report.summary.input_r),
1178        ("core admissions", report.summary.core_admissions),
1179        ("frames captured", report.summary.frames_captured),
1180        ("core errors", report.summary.core_errors),
1181        ("payload AW", report.summary.payload_aw),
1182        ("payload B", report.summary.payload_b),
1183        ("memory completions", report.summary.memory_completed),
1184    ] {
1185        ensure!(
1186            u32::from(value) <= batch,
1187            "{label} exceeds the frozen batch"
1188        );
1189    }
1190    ensure!(
1191        report.summary.input_r <= report.summary.input_ar,
1192        "input R count exceeds accepted input addresses"
1193    );
1194    ensure!(
1195        report.summary.core_admissions <= report.summary.input_r,
1196        "signer admissions exceed canonical input responses"
1197    );
1198    ensure!(
1199        u32::from(report.summary.frames_captured) + u32::from(report.summary.core_errors)
1200            <= u32::from(report.summary.core_admissions),
1201        "signer completions exceed admissions"
1202    );
1203    ensure!(
1204        report.summary.payload_aw == report.summary.frames_captured
1205            && report.summary.payload_b == report.summary.payload_aw
1206            && report.summary.memory_completed <= report.summary.payload_b,
1207        "payload retirement counters are internally contradictory"
1208    );
1209    ensure!(
1210        report.summary.payload_w == u32::from(report.summary.payload_aw) * PAYLOAD_W_BEATS_PER_JOB,
1211        "payload W count differs from complete 35-beat frames"
1212    );
1213    if scenario != Scenario::InputError {
1214        ensure!(
1215            report.summary.input_ar == report.summary.input_r,
1216            "closed non-input-fault case has unmatched input transactions"
1217        );
1218    }
1219    Ok(())
1220}
1221
1222fn validate_memory_timing(report: &SourceSimulationRecord) -> Result<()> {
1223    match report.summary.memory_completed {
1224        0 => ensure!(
1225            report.memory_first == 0 && report.memory_last == 0 && report.memory_span == 0,
1226            "zero-completion case contains memory-retirement timing"
1227        ),
1228        1 => ensure!(
1229            report.memory_first > 0
1230                && report.memory_last == report.memory_first
1231                && report.memory_last <= report.summary.payload_cycles
1232                && report.memory_span == 0,
1233            "single-completion memory-retirement timing is inconsistent"
1234        ),
1235        _ => ensure!(
1236            report.memory_first > 0
1237                && report.memory_last >= report.memory_first
1238                && report.memory_last <= report.summary.payload_cycles
1239                && report.memory_span == report.memory_last - report.memory_first,
1240            "multi-completion memory-retirement timing is inconsistent"
1241        ),
1242    }
1243    Ok(())
1244}
1245
1246fn validate_capture_timing(report: &SourceSimulationRecord) -> Result<()> {
1247    match report.summary.frames_captured {
1248        0 => ensure!(
1249            report.summary.core_latency_cycles == 0 && report.summary.capture_span_cycles == 0,
1250            "zero-frame case contains capture timing"
1251        ),
1252        1 => ensure!(
1253            report.summary.core_latency_cycles > 0 && report.summary.capture_span_cycles == 0,
1254            "single-frame capture timing is inconsistent"
1255        ),
1256        _ => ensure!(
1257            report.summary.core_latency_cycles > 0 && report.summary.capture_span_cycles > 0,
1258            "multi-frame capture timing is inconsistent"
1259        ),
1260    }
1261    Ok(())
1262}
1263
1264fn validate_binary_geometry(
1265    request: &SourceOracleRequest,
1266    inputs: &[u8],
1267    outputs: &[u8],
1268    summary: &[u8],
1269) -> Result<()> {
1270    let jobs = usize::try_from(request.batch).context("batch does not fit this host")?;
1271    ensure!(
1272        inputs.len()
1273            == jobs
1274                .checked_mul(INPUT_BYTES_PER_JOB)
1275                .context("input length overflow")?,
1276        "inputs.bin length differs from the frozen geometry"
1277    );
1278    ensure!(
1279        outputs.len()
1280            == jobs
1281                .max(1)
1282                .checked_mul(OUTPUT_SLOT_BYTES_PER_JOB)
1283                .context("output length overflow")?,
1284        "outputs.bin length differs from the frozen geometry"
1285    );
1286    ensure!(
1287        summary.len() == SUMMARY_BYTES,
1288        "summary.bin length differs from the frozen geometry"
1289    );
1290    Ok(())
1291}
1292
1293fn validate_regenerated_inputs(seed: u64, batch: u32, inputs: &[u8]) -> Result<()> {
1294    let mut private_seed_digests = HashSet::new();
1295    for job in 0..batch {
1296        let expected = generated_job(seed, job);
1297        let offset = usize::try_from(job)
1298            .context("job index does not fit this host")?
1299            .checked_mul(INPUT_BYTES_PER_JOB)
1300            .context("input offset overflow")?;
1301        let observed = &inputs[offset..offset + INPUT_BYTES_PER_JOB];
1302        ensure!(
1303            observed == expected.as_slice(),
1304            "inputs.bin differs from deterministic generation at job {job}"
1305        );
1306        let digest: [u8; 32] = Sha256::digest(&expected[..32]).into();
1307        ensure!(
1308            private_seed_digests.insert(digest),
1309            "source case reuses a private WOTS seed"
1310        );
1311    }
1312    Ok(())
1313}
1314
1315fn validate_summary_bytes(report: &SourceSimulationRecord, seed: u64, bytes: &[u8]) -> Result<()> {
1316    ensure!(&bytes[..4] == b"HSR1", "summary magic is not HSR1");
1317    ensure!(read_u16(bytes, 4) == 1, "summary ABI revision is not one");
1318    ensure!(bytes[6] == 1, "summary profile wire tag is not SHA-256");
1319    let decoded = DecodedSummary {
1320        terminal_code: bytes[7],
1321        nonce: read_u64(bytes, 8),
1322        payload_cycles: read_u64(bytes, 16),
1323        core_latency_cycles: read_u64(bytes, 24),
1324        capture_span_cycles: read_u64(bytes, 32),
1325        batch: read_u16(bytes, 40),
1326        input_ar: read_u16(bytes, 42),
1327        input_r: read_u16(bytes, 44),
1328        core_admissions: read_u16(bytes, 46),
1329        frames_captured: read_u16(bytes, 48),
1330        core_errors: read_u16(bytes, 50),
1331        payload_aw: read_u16(bytes, 52),
1332        payload_b: read_u16(bytes, 54),
1333        payload_w: read_u32(bytes, 56),
1334        memory_completed: read_u16(bytes, 60),
1335        error_flags: read_u16(bytes, 62),
1336    };
1337    ensure!(
1338        decoded == report.summary,
1339        "summary.bin differs from the strict source report"
1340    );
1341    ensure!(
1342        decoded.nonce == seed ^ NONCE_DOMAIN,
1343        "binary summary nonce differs from deterministic input"
1344    );
1345    Ok(())
1346}
1347
1348fn expected_payload(oracle: Sha256GenericWots, input: &[u8]) -> Result<[u8; FUSED_OUTPUT_BYTES]> {
1349    let private_seed: [u8; 32] = input[..32]
1350        .try_into()
1351        .context("input private-seed span has the wrong size")?;
1352    let message: [u8; 32] = input[32..]
1353        .try_into()
1354        .context("input message span has the wrong size")?;
1355    let fused = oracle.sign_and_public_key_from_private_seed(
1356        PrivateSeed::<HashSigsSha256GenericV1>::new(private_seed),
1357        &MessageDigest::new(message),
1358    );
1359    let signature = fused.signature.to_bytes();
1360    let public_key = fused.public_key.to_bytes();
1361    let mut expected = [0_u8; FUSED_OUTPUT_BYTES];
1362    expected[..SIGNATURE_BYTES].copy_from_slice(&signature);
1363    expected[SIGNATURE_BYTES..].copy_from_slice(&public_key);
1364    Ok(expected)
1365}
1366
1367fn parse_seed(text: &str) -> Result<u64> {
1368    let hex = text
1369        .strip_prefix("0x")
1370        .context("source seed lacks the 0x prefix")?;
1371    ensure!(
1372        is_lower_hex(hex, 16),
1373        "source seed is not fixed-width lowercase hexadecimal"
1374    );
1375    u64::from_str_radix(hex, 16).context("source seed does not fit u64")
1376}
1377
1378fn generated_job(seed: u64, job: u32) -> [u8; INPUT_BYTES_PER_JOB] {
1379    let mut output = [0_u8; INPUT_BYTES_PER_JOB];
1380    for (lane, byte) in output.iter_mut().enumerate() {
1381        let lane = u64::try_from(lane).expect("input lane fits u64");
1382        let mut value = seed
1383            ^ u64::from(job).wrapping_mul(0x9e37_79b9_7f4a_7c15)
1384            ^ lane.wrapping_mul(0xd6e8_feb8_6659_fd93)
1385            ^ if lane < 32 {
1386                0x7365_6564_5f76_3031
1387            } else {
1388                0x6d65_7373_5f76_3031
1389            };
1390        value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
1391        value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
1392        *byte = (value ^ (value >> 31)).to_le_bytes()[0];
1393    }
1394    output
1395}
1396
1397fn source_poison(seed: u64, output_offset: usize) -> u8 {
1398    let lane = u64::try_from(output_offset).expect("bounded output offset fits u64");
1399    entropy(seed ^ WRITE_MODEL_DOMAIN ^ POISON_DOMAIN, lane, 0x31).to_le_bytes()[0]
1400}
1401
1402fn entropy(seed: u64, cycle: u64, stream: u64) -> u64 {
1403    let mut value = seed
1404        ^ cycle.wrapping_mul(0x9e37_79b9_7f4a_7c15)
1405        ^ stream.wrapping_mul(0xd6e8_feb8_6659_fd93);
1406    value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
1407    value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
1408    value ^ (value >> 31)
1409}
1410
1411fn read_u16(bytes: &[u8], offset: usize) -> u16 {
1412    u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
1413}
1414
1415fn read_u32(bytes: &[u8], offset: usize) -> u32 {
1416    u32::from_le_bytes([
1417        bytes[offset],
1418        bytes[offset + 1],
1419        bytes[offset + 2],
1420        bytes[offset + 3],
1421    ])
1422}
1423
1424fn read_u64(bytes: &[u8], offset: usize) -> u64 {
1425    u64::from_le_bytes(
1426        bytes[offset..offset + 8]
1427            .try_into()
1428            .expect("summary span contains eight bytes"),
1429    )
1430}
1431
1432fn merge_words(low: u32, high: u32) -> u64 {
1433    u64::from(low) | (u64::from(high) << 32)
1434}
1435
1436fn manifest_binding(record: &FileRecord) -> ManifestBinding {
1437    ManifestBinding {
1438        file: "SHA256SUMS",
1439        bytes: record.bytes,
1440        sha256: record.sha256.clone(),
1441    }
1442}
1443
1444fn canonical_json_line<T: Serialize>(value: &T) -> Result<Vec<u8>> {
1445    let mut bytes = serde_json::to_vec(value).context("cannot serialize source-oracle JSON")?;
1446    bytes.push(b'\n');
1447    Ok(bytes)
1448}
1449
1450#[cfg(test)]
1451mod tests {
1452    use super::{Scenario, parse_seed, validate_job_list};
1453
1454    #[test]
1455    fn retirement_list_rejects_duplicates_order_reversal_and_range_errors() {
1456        assert!(validate_job_list(&[0, 1], 2, true).is_ok());
1457        assert!(validate_job_list(&[0, 0], 2, false).is_err());
1458        assert!(validate_job_list(&[1, 0], 2, false).is_err());
1459        assert!(validate_job_list(&[2], 2, false).is_err());
1460        assert!(validate_job_list(&[0], 2, true).is_err());
1461    }
1462
1463    #[test]
1464    fn source_seed_requires_fixed_width_lowercase_hex() {
1465        assert_eq!(parse_seed("0x0000000000000011").unwrap(), 17);
1466        assert!(parse_seed("11").is_err());
1467        assert!(parse_seed("0x11").is_err());
1468        assert!(parse_seed("0x00000000000000AA").is_err());
1469    }
1470
1471    #[test]
1472    fn frozen_scenarios_define_the_complete_payload_write_prefix() {
1473        for (scenario, expected) in [
1474            (Scenario::SuccessBatch0, 0),
1475            (Scenario::SuccessBatch1, 1),
1476            (Scenario::SuccessBatch6, 6),
1477            (Scenario::SoftAbort, 0),
1478            (Scenario::InputError, 0),
1479            (Scenario::SignerError, 0),
1480            (Scenario::PayloadBError, 1),
1481            (Scenario::SummaryBError, 1),
1482        ] {
1483            assert_eq!(scenario.written_payload_jobs(), expected);
1484        }
1485    }
1486}