1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![allow(clippy::large_types_passed_by_value, clippy::too_many_lines)] mod 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
40pub const PROFILE_NAME: &str = "HASHSIGS_SHA256_GENERIC_V1";
42pub const MAX_MODELED_CYCLES: u64 = 60_000;
44pub const SHA_REQUESTS_PER_JOB: u64 = 1_258;
46pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
62#[serde(rename_all = "snake_case")]
63pub enum Scenario {
64 SuccessBatch0,
66 SuccessBatch1,
68 SuccessBatch6,
70 SoftAbort,
72 InputError,
74 SignerError,
76 PayloadBError,
78 SummaryBError,
80}
81
82impl Scenario {
83 #[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 #[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 #[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 #[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
133pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub struct SimulationCase {
148 pub scenario: Scenario,
150 pub seed: u64,
152}
153
154impl SimulationCase {
155 #[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#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
177pub struct DecodedSummary {
178 pub terminal_code: u8,
180 pub nonce: u64,
182 pub payload_cycles: u64,
184 pub core_latency_cycles: u64,
186 pub capture_span_cycles: u64,
188 pub batch: u16,
190 pub input_ar: u16,
192 pub input_r: u16,
194 pub core_admissions: u16,
196 pub frames_captured: u16,
198 pub core_errors: u16,
200 pub payload_aw: u16,
202 pub payload_b: u16,
204 pub payload_w: u32,
206 pub memory_completed: u16,
208 pub error_flags: u16,
210}
211
212#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
214pub struct SimulationReport {
215 pub schema: u64,
217 pub kind: &'static str,
219 pub evidence_tier: &'static str,
221 pub source_simulation_valid: bool,
223 pub cryptographic_oracle_valid: Option<bool>,
225 pub hardware_completion_promotable: bool,
227 pub independent_rust_oracle_required: bool,
229 pub profile: &'static str,
231 pub backpressure_model: &'static str,
233 pub case: String,
235 pub seed: String,
237 pub batch: u32,
239 pub expected_success: bool,
241 pub maximum_modeled_cycles: u64,
243 pub modeled_cycles: u64,
245 pub sha_requests: u64,
247 pub payload_w_beats: u32,
249 pub completed_jobs: Vec<u32>,
251 pub summary: DecodedSummary,
253 pub summary_b_canonical: bool,
255 pub terminal_registers: BTreeMap<String, u32>,
257 pub memory_first: u64,
259 pub memory_last: u64,
261 pub memory_span: u64,
263}
264
265#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct CaseArtifacts {
268 pub report: SimulationReport,
270 pub inputs: Vec<u8>,
272 pub outputs: Vec<u8>,
274 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 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(®isters, "memory_first")?,
632 memory_last: merge_register_words(®isters, "memory_last")?,
633 memory_span: merge_register_words(®isters, "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
860pub 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
876pub 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
890pub 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}