Skip to main content

u280_hbm_sim/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![allow(clippy::large_types_passed_by_value, clippy::too_many_lines)] // The source harness mirrors wide hardware ports and audits flat AXI transitions.
4
5//! The harness is deliberately outside both production components. It owns
6//! independent simulation state for the shell and signer and connects only
7//! their typed public ports on each modeled clock cycle.
8
9mod artifact;
10mod model;
11
12use std::collections::BTreeMap;
13use std::fmt;
14use std::path::Path;
15
16use anyhow::{Context, Result, ensure};
17use rhdl::prelude::*;
18use serde::Serialize;
19use u280_shell_rhdl::{
20    HbmShellInput, U280HbmShell,
21    abi::{
22        ERROR_INPUT_AXI, ERROR_OUTPUT_AXI, ERROR_SIGNER_COMPLETION, ERROR_SOFT_ABORT,
23        ERROR_SUMMARY_AXI, OUTPUT_PAYLOAD_BYTES, OUTPUT_SLOT_BYTES, TERMINAL_ABORTED,
24        TERMINAL_INPUT_AXI, TERMINAL_OUTPUT_AXI, TERMINAL_SIGNER, TERMINAL_SUCCESS,
25        TERMINAL_SUMMARY_AXI,
26    },
27    control::last_status_is_host_success_kernel,
28};
29use wots_rhdl::tasks::TOTAL_TASKS;
30use wots_rhdl::{
31    SIGNER_FRAME_BEATS, Sha256SignerTop, SignerErrorCompletion, SignerTopInput, SignerTopOutput,
32};
33
34use artifact::merge_register_words;
35pub use artifact::{write_case_artifacts, write_suite_index};
36use model::{
37    ControlDriver, ReadFault, ReadMemory, WriteFault, WriteMemory, assert_no_pending_memory,
38};
39
40/// Cryptographic profile transported by the production HBM shell.
41pub const PROFILE_NAME: &str = "HASHSIGS_SHA256_GENERIC_V1";
42/// Hard ceiling applied independently to every modeled case.
43pub const MAX_MODELED_CYCLES: u64 = 60_000;
44/// Exact fused-signing SHA-256 compression requests per admitted job.
45pub const SHA_REQUESTS_PER_JOB: u64 = 1_258;
46/// Exact payload W handshakes per successful job.
47pub const PAYLOAD_W_BEATS_PER_JOB: u64 = 35;
48
49const SIMULATION_STACK_BYTES: usize = 768 * 1024 * 1024;
50
51const _: () = {
52    assert!(TOTAL_TASKS == 1_258);
53    assert!(SHA_REQUESTS_PER_JOB == 1_258);
54    assert!(SIGNER_FRAME_BEATS == 35);
55    assert!(PAYLOAD_W_BEATS_PER_JOB == 35);
56    assert!(OUTPUT_PAYLOAD_BYTES == 2_208);
57    assert!(OUTPUT_SLOT_BYTES == 4_096);
58};
59
60/// One deterministic scenario in the bounded standard suite.
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
62#[serde(rename_all = "snake_case")]
63pub enum Scenario {
64    /// Valid zero-job launch and summary-only completion.
65    SuccessBatch0,
66    /// One valid signing job.
67    SuccessBatch1,
68    /// Six valid jobs with exact work and transport accounting.
69    SuccessBatch6,
70    /// AXI4-Lite soft abort after the first signer admission.
71    SoftAbort,
72    /// First HBM0 response is `SLVERR`.
73    InputError,
74    /// Explicit signer error completion for an active admitted job.
75    SignerError,
76    /// First payload B response is `SLVERR`.
77    PayloadBError,
78    /// Terminal summary B response is `SLVERR`.
79    SummaryBError,
80}
81
82impl Scenario {
83    /// Stable command-line and artifact name.
84    #[must_use]
85    pub const fn name(self) -> &'static str {
86        match self {
87            Self::SuccessBatch0 => "success_batch_0",
88            Self::SuccessBatch1 => "success_batch_1",
89            Self::SuccessBatch6 => "success_batch_6",
90            Self::SoftAbort => "soft_abort",
91            Self::InputError => "input_error",
92            Self::SignerError => "signer_error",
93            Self::PayloadBError => "payload_b_error",
94            Self::SummaryBError => "summary_b_error",
95        }
96    }
97
98    /// Frozen job count for this case.
99    #[must_use]
100    pub const fn batch(self) -> u32 {
101        match self {
102            Self::SuccessBatch0 => 0,
103            Self::SuccessBatch6 | Self::SoftAbort => 6,
104            _ => 1,
105        }
106    }
107
108    /// Whether every requested job must retire successfully.
109    #[must_use]
110    pub const fn expected_success(self) -> bool {
111        matches!(
112            self,
113            Self::SuccessBatch0 | Self::SuccessBatch1 | Self::SuccessBatch6
114        )
115    }
116
117    /// Parse one stable case name.
118    #[must_use]
119    pub fn from_name(name: &str) -> Option<Self> {
120        STANDARD_SCENARIOS
121            .iter()
122            .copied()
123            .find(|scenario| scenario.name() == name)
124    }
125}
126
127impl fmt::Display for Scenario {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        formatter.write_str(self.name())
130    }
131}
132
133/// Ordered standard-suite inventory.
134pub const STANDARD_SCENARIOS: [Scenario; 8] = [
135    Scenario::SuccessBatch0,
136    Scenario::SuccessBatch1,
137    Scenario::SuccessBatch6,
138    Scenario::SoftAbort,
139    Scenario::InputError,
140    Scenario::SignerError,
141    Scenario::PayloadBError,
142    Scenario::SummaryBError,
143];
144
145/// User-selected deterministic case configuration.
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub struct SimulationCase {
148    /// Scenario contract.
149    pub scenario: Scenario,
150    /// Seed for input generation, poison bytes, and stateless backpressure.
151    pub seed: u64,
152}
153
154impl SimulationCase {
155    /// Construct the standard seed for one scenario.
156    #[must_use]
157    pub const fn standard(scenario: Scenario) -> Self {
158        let ordinal = match scenario {
159            Scenario::SuccessBatch0 => 0,
160            Scenario::SuccessBatch1 => 1,
161            Scenario::SuccessBatch6 => 2,
162            Scenario::SoftAbort => 3,
163            Scenario::InputError => 4,
164            Scenario::SignerError => 5,
165            Scenario::PayloadBError => 6,
166            Scenario::SummaryBError => 7,
167        };
168        Self {
169            scenario,
170            seed: 0x6873_7231_5f68_626d_u64 ^ (ordinal * 0x0101_0101_0101_0101),
171        }
172    }
173}
174
175/// Strictly decoded `HSR1` terminal summary.
176#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
177pub struct DecodedSummary {
178    /// Terminal status serialized before the summary B response.
179    pub terminal_code: u8,
180    /// Opaque launch nonce.
181    pub nonce: u64,
182    /// Execute/abort interval.
183    pub payload_cycles: u64,
184    /// First signer admission through last capture.
185    pub core_latency_cycles: u64,
186    /// First-to-last capture span.
187    pub capture_span_cycles: u64,
188    /// Frozen batch.
189    pub batch: u16,
190    /// Input AR handshakes.
191    pub input_ar: u16,
192    /// Canonical input R handshakes.
193    pub input_r: u16,
194    /// Signer admissions.
195    pub core_admissions: u16,
196    /// Complete frames captured.
197    pub frames_captured: u16,
198    /// Explicit signer error completions.
199    pub core_errors: u16,
200    /// Payload AW handshakes.
201    pub payload_aw: u16,
202    /// Payload B handshakes.
203    pub payload_b: u16,
204    /// Payload W handshakes.
205    pub payload_w: u32,
206    /// Canonical payload completions.
207    pub memory_completed: u16,
208    /// Sticky error flags serialized before summary B.
209    pub error_flags: u16,
210}
211
212/// Machine-readable result for one source simulation.
213#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
214pub struct SimulationReport {
215    /// Schema revision.
216    pub schema: u64,
217    /// Fixed record kind.
218    pub kind: &'static str,
219    /// Evidence tier; always source simulation.
220    pub evidence_tier: &'static str,
221    /// True only after every case-specific source-model assertion passed.
222    pub source_simulation_valid: bool,
223    /// Always null until a separate Rust oracle completes comparison.
224    pub cryptographic_oracle_valid: Option<bool>,
225    /// Always false for this crate.
226    pub hardware_completion_promotable: bool,
227    /// Independent cryptographic comparison remains required.
228    pub independent_rust_oracle_required: bool,
229    /// Frozen profile name.
230    pub profile: &'static str,
231    /// Versioned deterministic decision function used by both HBM models.
232    pub backpressure_model: &'static str,
233    /// Stable case name.
234    pub case: String,
235    /// Deterministic seed as fixed-width lowercase hexadecimal.
236    pub seed: String,
237    /// Frozen launch batch.
238    pub batch: u32,
239    /// Whether the run was expected to close successfully.
240    pub expected_success: bool,
241    /// Per-case hard cycle ceiling.
242    pub maximum_modeled_cycles: u64,
243    /// Cycles actually modeled, including control setup and terminal reads.
244    pub modeled_cycles: u64,
245    /// Exact accepted signer SHA requests observed from production simulation state.
246    pub sha_requests: u64,
247    /// Payload-only HBM1 W handshakes.
248    pub payload_w_beats: u32,
249    /// Successfully retired payload job IDs.
250    pub completed_jobs: Vec<u32>,
251    /// Exact decoded summary bytes.
252    pub summary: DecodedSummary,
253    /// Whether the separately observed summary B response was canonical.
254    pub summary_b_canonical: bool,
255    /// AXI4-Lite terminal register snapshot.
256    pub terminal_registers: BTreeMap<String, u32>,
257    /// Canonical payload-B retirement first cycle.
258    pub memory_first: u64,
259    /// Canonical payload-B retirement last cycle.
260    pub memory_last: u64,
261    /// Canonical payload-B retirement span.
262    pub memory_span: u64,
263}
264
265/// Raw files and report produced by one completed case.
266#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct CaseArtifacts {
268    /// Machine-readable source-tier report.
269    pub report: SimulationReport,
270    /// Contiguous 64-byte input jobs.
271    pub inputs: Vec<u8>,
272    /// Complete 4 KiB output slots, or one poisoned allocation sentinel for batch zero.
273    pub outputs: Vec<u8>,
274    /// Exact terminal summary transfer.
275    pub summary: [u8; 64],
276}
277
278fn read_u16(bytes: &[u8], offset: usize) -> u16 {
279    u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
280}
281
282fn read_u32(bytes: &[u8], offset: usize) -> u32 {
283    u32::from_le_bytes([
284        bytes[offset],
285        bytes[offset + 1],
286        bytes[offset + 2],
287        bytes[offset + 3],
288    ])
289}
290
291fn read_u64(bytes: &[u8], offset: usize) -> u64 {
292    u64::from_le_bytes(
293        bytes[offset..offset + 8]
294            .try_into()
295            .expect("summary slice has eight bytes"),
296    )
297}
298
299fn decode_summary(bytes: &[u8; 64]) -> Result<DecodedSummary> {
300    ensure!(&bytes[..4] == b"HSR1", "summary magic was not HSR1");
301    ensure!(read_u16(bytes, 4) == 1, "summary ABI revision was not one");
302    ensure!(bytes[6] == 1, "summary profile wire tag was not SHA-256");
303    Ok(DecodedSummary {
304        terminal_code: bytes[7],
305        nonce: read_u64(bytes, 8),
306        payload_cycles: read_u64(bytes, 16),
307        core_latency_cycles: read_u64(bytes, 24),
308        capture_span_cycles: read_u64(bytes, 32),
309        batch: read_u16(bytes, 40),
310        input_ar: read_u16(bytes, 42),
311        input_r: read_u16(bytes, 44),
312        core_admissions: read_u16(bytes, 46),
313        frames_captured: read_u16(bytes, 48),
314        core_errors: read_u16(bytes, 50),
315        payload_aw: read_u16(bytes, 52),
316        payload_b: read_u16(bytes, 54),
317        payload_w: read_u32(bytes, 56),
318        memory_completed: read_u16(bytes, 60),
319        error_flags: read_u16(bytes, 62),
320    })
321}
322
323fn generated_job(seed: u64, job: u32) -> [u8; 64] {
324    let mut output = [0_u8; 64];
325    for (lane, byte) in output.iter_mut().enumerate() {
326        let lane = u64::try_from(lane).expect("input lane fits u64");
327        let mut value = seed
328            ^ u64::from(job).wrapping_mul(0x9e37_79b9_7f4a_7c15)
329            ^ lane.wrapping_mul(0xd6e8_feb8_6659_fd93)
330            ^ if lane < 32 {
331                0x7365_6564_5f76_3031
332            } else {
333                0x6d65_7373_5f76_3031
334            };
335        value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
336        value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
337        *byte = (value ^ (value >> 31)).to_le_bytes()[0];
338    }
339    output
340}
341
342#[derive(Clone, Copy, Debug)]
343struct Admission {
344    job_id: u32,
345    cluster: u8,
346}
347
348struct Harness {
349    case: SimulationCase,
350    nonce: u64,
351    shell: U280HbmShell,
352    shell_state: <U280HbmShell as Synchronous>::S,
353    signer: Sha256SignerTop,
354    signer_state: <Sha256SignerTop as Synchronous>::S,
355    control: ControlDriver,
356    read_memory: ReadMemory,
357    write_memory: WriteMemory,
358    jobs: Vec<[u8; 64]>,
359    cycle: u64,
360    sha_requests: u64,
361    first_admission: Option<Admission>,
362    abort_queued: bool,
363    signer_error_injected: bool,
364    terminal_reads_queued: bool,
365}
366
367impl Harness {
368    fn new(case: SimulationCase) -> Self {
369        let batch = case.scenario.batch();
370        let jobs: Vec<_> = (0..batch)
371            .map(|job| generated_job(case.seed, job))
372            .collect();
373        let read_fault = if case.scenario == Scenario::InputError {
374            ReadFault::FirstResponse
375        } else {
376            ReadFault::None
377        };
378        let mut write_memory = WriteMemory::new(case.seed ^ 0x5752_4954_455f_5631, jobs.len());
379        write_memory.set_fault(match case.scenario {
380            Scenario::PayloadBError => WriteFault::FirstPayloadResponse,
381            Scenario::SummaryBError => WriteFault::SummaryResponse,
382            _ => WriteFault::None,
383        });
384        let nonce = case.seed ^ 0x4e4f_4e43_455f_5631;
385        let shell = U280HbmShell::default();
386        let shell_state = shell.init();
387        let signer = Sha256SignerTop::default();
388        let signer_state = signer.init();
389        Self {
390            case,
391            nonce,
392            shell,
393            shell_state,
394            signer,
395            signer_state,
396            control: ControlDriver::new(batch, nonce),
397            read_memory: ReadMemory::new(
398                case.seed ^ 0x5245_4144_5f56_3031,
399                jobs.clone(),
400                read_fault,
401            ),
402            write_memory,
403            jobs,
404            cycle: 0,
405            sha_requests: 0,
406            first_admission: None,
407            abort_queued: false,
408            signer_error_injected: false,
409            terminal_reads_queued: false,
410        }
411    }
412
413    fn maybe_inject_signer_error(
414        &mut self,
415        actual: SignerTopOutput,
416        shell_signals: SignerTopInput,
417        accepted_requests_now: u64,
418    ) -> Result<SignerTopOutput> {
419        if self.case.scenario != Scenario::SignerError || self.signer_error_injected {
420            return Ok(actual);
421        }
422        let Some(admission) = self.first_admission else {
423            return Ok(actual);
424        };
425        if self.sha_requests + accepted_requests_now < 8 || !shell_signals.error_ready {
426            return Ok(actual);
427        }
428        ensure!(
429            !actual.output_valid && !actual.error_valid,
430            "signer-error injection collided with a real signer completion"
431        );
432        let mut injected = actual;
433        injected.error_valid = true;
434        injected.error = SignerErrorCompletion {
435            job_id: b32(u128::from(admission.job_id)),
436            cluster: b2(u128::from(admission.cluster)),
437            context: b2(0),
438        };
439        injected.delivered_error = true;
440        self.signer_error_injected = true;
441        Ok(injected)
442    }
443
444    fn step(&mut self) -> Result<()> {
445        let global_reset = self.cycle < 2;
446        let control_input = if global_reset {
447            Default::default()
448        } else {
449            self.control.drive()
450        };
451        let read_input = self.read_memory.drive(self.cycle);
452        let write_input = self.write_memory.drive(self.cycle);
453        let low = clock_reset(clock(false), reset(global_reset));
454
455        // First expose signer signals that are functions only of retained
456        // signer state. The second low-phase evaluation supplies the shell's
457        // exact combinational ready/valid inputs before either rising edge.
458        let signer_probe = self
459            .signer
460            .sim(low, SignerTopInput::default(), &mut self.signer_state);
461        let preliminary_input = HbmShellInput {
462            control: control_input,
463            input_hbm: read_input,
464            output_hbm: write_input,
465            signer: signer_probe,
466        };
467        let preliminary_shell = self
468            .shell
469            .sim(low, preliminary_input, &mut self.shell_state);
470        let signer_reset = global_reset || preliminary_shell.signer_soft_reset;
471        let signer_low = clock_reset(clock(false), reset(signer_reset));
472        let actual_signer =
473            self.signer
474                .sim(signer_low, preliminary_shell.signer, &mut self.signer_state);
475        let accepted_requests_now =
476            Sha256SignerTop::simulation_request_accepted(&self.signer_state)
477                .iter()
478                .flatten()
479                .filter(|accepted| **accepted)
480                .count();
481        let accepted_requests_now =
482            u64::try_from(accepted_requests_now).expect("accepted request count fits u64");
483
484        if actual_signer.start_accepted && self.first_admission.is_none() {
485            self.first_admission = Some(Admission {
486                job_id: u32::try_from(preliminary_shell.signer.job_id.raw())
487                    .expect("signer job ID fits u32"),
488                cluster: u8::try_from(actual_signer.start_cluster.raw())
489                    .expect("signer cluster fits u8"),
490            });
491        }
492        let presented_signer = self.maybe_inject_signer_error(
493            actual_signer,
494            preliminary_shell.signer,
495            accepted_requests_now,
496        )?;
497        let final_input = HbmShellInput {
498            signer: presented_signer,
499            ..preliminary_input
500        };
501        let shell_output = self.shell.sim(low, final_input, &mut self.shell_state);
502        ensure!(
503            shell_output.signer == preliminary_shell.signer
504                && shell_output.signer_soft_reset == preliminary_shell.signer_soft_reset,
505            "shell/signer low-phase fixed point changed after signer presentation"
506        );
507
508        if !global_reset {
509            self.control.observe(control_input, shell_output.control)?;
510            self.read_memory
511                .observe(self.cycle, read_input, shell_output.input_hbm)?;
512            self.write_memory
513                .observe(self.cycle, write_input, shell_output.output_hbm)?;
514        }
515        self.sha_requests = self
516            .sha_requests
517            .checked_add(accepted_requests_now)
518            .context("SHA request count overflow")?;
519
520        let signer_high = clock_reset(clock(true), reset(signer_reset));
521        let high = clock_reset(clock(true), reset(global_reset));
522        let _ = self
523            .signer
524            .sim(signer_high, shell_output.signer, &mut self.signer_state);
525        let _ = self.shell.sim(high, final_input, &mut self.shell_state);
526        self.cycle += 1;
527
528        if self.case.scenario == Scenario::SoftAbort && self.sha_requests >= 8 && !self.abort_queued
529        {
530            self.control.enqueue_abort();
531            self.abort_queued = true;
532        }
533        if self.write_memory.summary_response_seen && !self.terminal_reads_queued {
534            self.control.enqueue_terminal_reads();
535            self.terminal_reads_queued = true;
536        }
537        Ok(())
538    }
539
540    fn complete(&self) -> bool {
541        self.write_memory.summary_response_seen && self.control.terminal_reads_complete()
542    }
543
544    fn into_artifacts(self) -> Result<CaseArtifacts> {
545        assert_no_pending_memory(&self.read_memory, &self.write_memory)?;
546        self.write_memory.assert_padding_untouched()?;
547        ensure!(
548            self.cycle <= MAX_MODELED_CYCLES,
549            "case exceeded its cycle ceiling"
550        );
551        let registers = self.control.terminal_registers()?;
552        let last_status = registers
553            .get("last_status")
554            .copied()
555            .context("last_status was not captured")?;
556        let summary = decode_summary(&self.write_memory.summary)?;
557        ensure!(
558            summary.nonce == self.nonce,
559            "summary nonce did not match the launch"
560        );
561        ensure!(
562            u32::from(summary.batch) == self.case.scenario.batch(),
563            "summary batch mismatch"
564        );
565        ensure!(
566            u32::from(summary.input_ar) == self.read_memory.address_handshakes,
567            "summary/input-model AR count mismatch"
568        );
569        let rejected_input_responses =
570            u32::from(u8::from(self.case.scenario == Scenario::InputError));
571        ensure!(
572            u32::from(summary.input_r) + rejected_input_responses
573                == self.read_memory.response_handshakes,
574            "summary/input-model R count mismatch"
575        );
576        ensure!(
577            u32::from(summary.payload_aw) == self.write_memory.payload_aw,
578            "summary/output-model payload AW count mismatch"
579        );
580        ensure!(
581            summary.payload_w == self.write_memory.payload_w,
582            "summary/output-model payload W count mismatch"
583        );
584        ensure!(
585            u32::from(summary.payload_b) == self.write_memory.payload_b,
586            "summary/output-model payload B count mismatch"
587        );
588        ensure!(
589            u32::from(summary.memory_completed)
590                == u32::try_from(self.write_memory.completed_jobs.len())
591                    .expect("completed job count fits u32"),
592            "summary/output-model completion count mismatch"
593        );
594        ensure!(
595            self.write_memory.summary_aw == 1
596                && self.write_memory.summary_w == 1
597                && self.write_memory.summary_b == 1,
598            "terminal summary did not complete exactly one AW/W/B transaction"
599        );
600        validate_outcome(
601            self.case.scenario,
602            last_status,
603            &summary,
604            self.write_memory.summary_response_canonical,
605            self.sha_requests,
606            &self.write_memory.completed_jobs,
607        )?;
608        let inputs = self.jobs.into_iter().flatten().collect();
609        Ok(CaseArtifacts {
610            report: SimulationReport {
611                schema: 1,
612                kind: "hashsigs_u280_mock_hbm_source_simulation",
613                evidence_tier: "rhdl_source_simulation",
614                source_simulation_valid: true,
615                cryptographic_oracle_valid: None,
616                hardware_completion_promotable: false,
617                independent_rust_oracle_required: true,
618                profile: PROFILE_NAME,
619                backpressure_model: "stateless_splitmix64_v1",
620                case: self.case.scenario.name().to_owned(),
621                seed: format!("0x{:016x}", self.case.seed),
622                batch: self.case.scenario.batch(),
623                expected_success: self.case.scenario.expected_success(),
624                maximum_modeled_cycles: MAX_MODELED_CYCLES,
625                modeled_cycles: self.cycle,
626                sha_requests: self.sha_requests,
627                payload_w_beats: self.write_memory.payload_w,
628                completed_jobs: self.write_memory.completed_jobs,
629                summary,
630                summary_b_canonical: self.write_memory.summary_response_canonical,
631                memory_first: merge_register_words(&registers, "memory_first")?,
632                memory_last: merge_register_words(&registers, "memory_last")?,
633                memory_span: merge_register_words(&registers, "memory_span")?,
634                terminal_registers: registers,
635            },
636            inputs,
637            outputs: self.write_memory.output_slots,
638            summary: self.write_memory.summary,
639        })
640    }
641}
642
643fn expected_terminal(scenario: Scenario) -> (u8, u16, bool) {
644    let narrow_flags = |flags| u16::try_from(flags).expect("ABI error flags fit u16");
645    match scenario {
646        Scenario::SuccessBatch0 | Scenario::SuccessBatch1 | Scenario::SuccessBatch6 => {
647            (TERMINAL_SUCCESS, 0, true)
648        }
649        Scenario::SoftAbort => (TERMINAL_ABORTED, narrow_flags(ERROR_SOFT_ABORT), true),
650        Scenario::InputError => (TERMINAL_INPUT_AXI, narrow_flags(ERROR_INPUT_AXI), true),
651        Scenario::SignerError => (TERMINAL_SIGNER, narrow_flags(ERROR_SIGNER_COMPLETION), true),
652        Scenario::PayloadBError => (TERMINAL_OUTPUT_AXI, narrow_flags(ERROR_OUTPUT_AXI), true),
653        Scenario::SummaryBError => (TERMINAL_SUMMARY_AXI, narrow_flags(ERROR_SUMMARY_AXI), false),
654    }
655}
656
657fn validate_outcome(
658    scenario: Scenario,
659    last_status: u32,
660    summary: &DecodedSummary,
661    summary_b_canonical: bool,
662    sha_requests: u64,
663    completed_jobs: &[u32],
664) -> Result<()> {
665    let (terminal, flags, canonical_summary_b) = expected_terminal(scenario);
666    let terminal_from_status = last_status.to_le_bytes()[0];
667    let flags_from_status =
668        u16::try_from((last_status >> 8) & 0xffff).expect("masked status flags fit u16");
669    ensure!(
670        terminal_from_status == terminal,
671        "LAST_STATUS terminal code did not match {scenario}"
672    );
673    ensure!(
674        flags_from_status == flags,
675        "LAST_STATUS error flags did not match {scenario}"
676    );
677    ensure!(
678        summary_b_canonical == canonical_summary_b,
679        "summary B canonicality did not match {scenario}"
680    );
681    let occurrence_bits = (last_status >> 24) & 0x0f;
682    let expected_occurrence = if canonical_summary_b { 0x0f } else { 0x07 };
683    ensure!(
684        occurrence_bits == expected_occurrence,
685        "LAST_STATUS summary occurrence bits did not match {scenario}"
686    );
687
688    if scenario == Scenario::SummaryBError {
689        ensure!(
690            summary.terminal_code == TERMINAL_SUCCESS && summary.error_flags == 0,
691            "summary-B failure must not retroactively alter transferred summary bytes"
692        );
693    } else {
694        ensure!(
695            summary.terminal_code == terminal,
696            "summary terminal mismatch"
697        );
698        ensure!(summary.error_flags == flags, "summary error flags mismatch");
699    }
700
701    if scenario.expected_success() {
702        ensure!(
703            last_status_is_host_success_kernel(b32(u128::from(last_status))),
704            "successful scenario lacked the sole host-success status"
705        );
706        let expected_requests = u64::from(scenario.batch()) * SHA_REQUESTS_PER_JOB;
707        let expected_w = u64::from(scenario.batch()) * PAYLOAD_W_BEATS_PER_JOB;
708        ensure!(
709            sha_requests == expected_requests,
710            "successful SHA request count mismatch"
711        );
712        ensure!(
713            u64::from(summary.payload_w) == expected_w,
714            "successful payload W count mismatch"
715        );
716        ensure!(
717            u32::from(summary.memory_completed) == scenario.batch(),
718            "successful memory completion count mismatch"
719        );
720        for (label, observed) in [
721            ("input AR", summary.input_ar),
722            ("input R", summary.input_r),
723            ("core admissions", summary.core_admissions),
724            ("frames captured", summary.frames_captured),
725            ("payload AW", summary.payload_aw),
726            ("payload B", summary.payload_b),
727        ] {
728            ensure!(
729                u32::from(observed) == scenario.batch(),
730                "successful {label} count mismatch"
731            );
732        }
733        ensure!(
734            summary.core_errors == 0,
735            "successful run contained a signer error"
736        );
737        ensure!(
738            completed_jobs == (0..scenario.batch()).collect::<Vec<_>>().as_slice(),
739            "successful completed-job order mismatch"
740        );
741    } else {
742        ensure!(
743            !last_status_is_host_success_kernel(b32(u128::from(last_status))),
744            "failure scenario accidentally reported host success"
745        );
746    }
747
748    match scenario {
749        Scenario::SuccessBatch6 => {
750            ensure!(
751                sha_requests == 7_548,
752                "batch 6 did not issue exactly 7,548 SHA requests"
753            );
754            ensure!(
755                summary.payload_w == 210,
756                "batch 6 did not issue exactly 210 W beats"
757            );
758        }
759        Scenario::InputError => {
760            ensure!(sha_requests == 0, "input error admitted signer work");
761            ensure!(
762                summary.input_ar == 1 && summary.input_r == 0 && summary.core_admissions == 0,
763                "input-error progress counters were not fail-closed"
764            );
765            ensure!(
766                summary.memory_completed == 0,
767                "input error retired a payload"
768            );
769        }
770        Scenario::SignerError => {
771            ensure!(
772                sha_requests > 0,
773                "early failure did not exercise signer work"
774            );
775            ensure!(
776                sha_requests < SHA_REQUESTS_PER_JOB,
777                "early failure waited for a complete signing job"
778            );
779            ensure!(
780                summary.core_admissions == 1
781                    && summary.core_errors == 1
782                    && summary.frames_captured == 0,
783                "signer-error progress counters were not exact"
784            );
785            ensure!(
786                summary.memory_completed == 0,
787                "early failure retired a payload"
788            );
789        }
790        Scenario::SoftAbort => {
791            ensure!(
792                sha_requests > 0,
793                "early failure did not exercise signer work"
794            );
795            ensure!(
796                sha_requests < SHA_REQUESTS_PER_JOB,
797                "early failure waited for a complete signing job"
798            );
799            ensure!(
800                summary.core_admissions > 0
801                    && summary.core_errors == 0
802                    && summary.frames_captured == 0,
803                "soft-abort progress counters were not fail-closed"
804            );
805            ensure!(
806                summary.memory_completed == 0,
807                "early failure retired a payload"
808            );
809        }
810        Scenario::PayloadBError => {
811            ensure!(
812                sha_requests == SHA_REQUESTS_PER_JOB,
813                "payload B case work mismatch"
814            );
815            ensure!(
816                summary.frames_captured == 1
817                    && summary.payload_aw == 1
818                    && summary.payload_w == 35
819                    && summary.payload_b == 1,
820                "payload B case did not transfer exactly one frame"
821            );
822            ensure!(
823                summary.memory_completed == 0,
824                "failed payload B retired memory"
825            );
826        }
827        Scenario::SummaryBError => {
828            ensure!(
829                sha_requests == SHA_REQUESTS_PER_JOB,
830                "summary B case work mismatch"
831            );
832            ensure!(
833                summary.frames_captured == 1
834                    && summary.payload_aw == 1
835                    && summary.payload_w == 35
836                    && summary.payload_b == 1
837                    && summary.memory_completed == 1,
838                "summary B case lost its successful payload accounting"
839            );
840        }
841        Scenario::SuccessBatch0 | Scenario::SuccessBatch1 => {}
842    }
843    Ok(())
844}
845
846fn run_case_inner(case: SimulationCase) -> Result<CaseArtifacts> {
847    let mut harness = Harness::new(case);
848    while harness.cycle < MAX_MODELED_CYCLES && !harness.complete() {
849        harness.step()?;
850    }
851    ensure!(
852        harness.complete(),
853        "{} did not complete within {} modeled cycles",
854        case.scenario,
855        MAX_MODELED_CYCLES
856    );
857    harness.into_artifacts()
858}
859
860/// Execute one source-simulation case on an explicitly sized stack.
861///
862/// # Errors
863///
864/// Returns an error for any protocol mismatch, accounting mismatch, terminal
865/// mismatch, cycle-cap violation, or simulation-thread panic.
866pub fn run_case(case: SimulationCase) -> Result<CaseArtifacts> {
867    std::thread::Builder::new()
868        .name(format!("u280-hbm-source-sim-{}", case.scenario))
869        .stack_size(SIMULATION_STACK_BYTES)
870        .spawn(move || run_case_inner(case))
871        .context("cannot spawn bounded source-simulation thread")?
872        .join()
873        .map_err(|_| anyhow::anyhow!("source-simulation thread panicked"))?
874}
875
876/// Execute all eight standard cases sequentially.
877///
878/// # Errors
879///
880/// Returns the first case failure.
881pub fn run_standard_suite() -> Result<Vec<CaseArtifacts>> {
882    STANDARD_SCENARIOS
883        .iter()
884        .copied()
885        .map(SimulationCase::standard)
886        .map(run_case)
887        .collect()
888}
889
890/// Write a complete standard suite below a new root directory.
891///
892/// # Errors
893///
894/// Returns an error when simulation fails, the root exists, or artifact
895/// serialization fails.
896pub fn run_and_write_standard_suite(directory: &Path) -> Result<Vec<SimulationReport>> {
897    ensure!(
898        !directory.exists(),
899        "refusing to replace suite directory {}",
900        directory.display()
901    );
902    let artifacts = run_standard_suite()?;
903    std::fs::create_dir(directory)
904        .with_context(|| format!("cannot create suite directory {}", directory.display()))?;
905    for case in &artifacts {
906        write_case_artifacts(&directory.join(&case.report.case), case)?;
907    }
908    let reports: Vec<_> = artifacts.into_iter().map(|case| case.report).collect();
909    write_suite_index(directory, &reports)?;
910    Ok(reports)
911}
912
913#[cfg(test)]
914mod tests {
915    use super::*;
916
917    #[test]
918    fn standard_inventory_is_stable_and_complete() {
919        assert_eq!(STANDARD_SCENARIOS.len(), 8);
920        for scenario in STANDARD_SCENARIOS {
921            assert_eq!(Scenario::from_name(scenario.name()), Some(scenario));
922            assert!(SimulationCase::standard(scenario).seed != 0);
923        }
924        assert_eq!(Scenario::from_name("unknown"), None);
925    }
926
927    #[test]
928    fn frozen_work_accounting_matches_production_constants() {
929        assert_eq!(SHA_REQUESTS_PER_JOB, 1_258);
930        assert_eq!(PAYLOAD_W_BEATS_PER_JOB, 35);
931        assert_eq!(6 * SHA_REQUESTS_PER_JOB, 7_548);
932        assert_eq!(6 * PAYLOAD_W_BEATS_PER_JOB, 210);
933    }
934}