1use 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#[derive(Clone, Debug)]
64pub struct SourceOracleConfig {
65 pub source_root: PathBuf,
67 pub result_directory: PathBuf,
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct SourceOracleOutcome {
74 pub cryptographic_oracle_valid: bool,
76 pub case: String,
78 pub batch: u32,
80 pub compared_jobs: u32,
85 pub all_unwritten_slots_valid: bool,
87 pub result_manifest_sha256: String,
89 pub result_manifest_bytes: u64,
91 pub result_directory: PathBuf,
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct SourceOracleFailure {
98 pub result_directory: PathBuf,
100 pub result_manifest_sha256: String,
102 pub result_manifest_bytes: u64,
104 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#[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#[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#[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#[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
398pub 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 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}