1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::{
5 collections::BTreeMap,
6 ffi::OsString,
7 fmt::Write as _,
8 fs::{self, OpenOptions},
9 io::Write as _,
10 path::{Path, PathBuf},
11 process::Command as ProcessCommand,
12 sync::atomic::{AtomicU64, Ordering},
13 time::{Duration, Instant},
14};
15
16use anyhow::{Context, Result, anyhow, bail, ensure};
17use serde::{Deserialize, Serialize};
18use sha2::{Digest as _, Sha256};
19
20mod benchmark_kernel;
21mod full_sha_check;
22mod full_signer_check;
23mod signer_throughput_check;
24mod u280_hbm;
25mod u280_hbm_check;
26mod u280_hbm_rtl_check;
27
28pub const HASHSIGS_REVISION: &str = "2d315dd4168804b7cbc51c51a1bf7ca27bf74140";
30
31pub const RHDL_REVISION: &str = "c99d5cc53269a247bbc675d0fbd766991d409f56";
33
34pub const GENERATED_VERILOG_EVIDENCE_TIER: &str = "generated-verilog";
36
37pub const USAGE: &str = "Usage:\n rhdl-gen list\n rhdl-gen generate <TARGET> --output <DIRECTORY> [--force]\n rhdl-gen check [--full] [--signer-full] [--temp-root <DIRECTORY>] [--keep]\n rhdl-gen signer-throughput --source-evidence <DIRECTORY> --output <NEW_DIRECTORY>\n rhdl-gen benchmark-kernel --output <NEW_DIRECTORY>\n rhdl-gen u280-link-graph --kernel <VERILOG> --kernel-manifest <JSON> --signer-evidence <DIRECTORY> --output <NEW_JSON>\n rhdl-gen u280-verify-link-graph --kernel <VERILOG> --kernel-manifest <JSON> --signer-evidence <DIRECTORY> --record <JSON>\n rhdl-gen u280-hbm-rtl-check --kernel <VERILOG> --kernel-manifest <JSON> --signer-evidence <DIRECTORY> --record <JSON> --output <NEW_DIRECTORY>";
39
40#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum Command {
43 List,
45 Generate {
47 target: String,
49 output: PathBuf,
51 force: bool,
53 },
54 Check {
58 temp_root: Option<PathBuf>,
60 keep: bool,
62 full: bool,
65 signer_full: bool,
68 },
69 SignerThroughput {
72 source_evidence: PathBuf,
75 output: PathBuf,
77 },
78 BenchmarkKernel {
81 output: PathBuf,
83 },
84 U280LinkGraph {
87 kernel: PathBuf,
89 kernel_manifest: PathBuf,
91 signer_evidence: PathBuf,
93 output: PathBuf,
95 },
96 U280VerifyLinkGraph {
98 kernel: PathBuf,
100 kernel_manifest: PathBuf,
102 signer_evidence: PathBuf,
104 record: PathBuf,
106 },
107 U280HbmRtlCheck {
110 kernel: PathBuf,
112 kernel_manifest: PathBuf,
114 signer_evidence: PathBuf,
116 record: PathBuf,
118 output: PathBuf,
120 },
121 #[doc(hidden)]
124 InternalSignerLower {
125 kind: String,
127 output: PathBuf,
129 source_commit: String,
131 },
132 Help,
134}
135
136#[derive(Clone, Copy)]
138pub struct TargetSpec {
139 pub name: &'static str,
141 pub verilog_filename: &'static str,
143 pub manifest_filename: &'static str,
145 pub profile: &'static str,
147 pub top_module: &'static str,
149 pub description: &'static str,
151 pub validation_scope: &'static str,
153 pub default_unit_generation: bool,
155 generate: fn() -> Result<String>,
156}
157
158impl std::fmt::Debug for TargetSpec {
159 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 formatter
161 .debug_struct("TargetSpec")
162 .field("name", &self.name)
163 .field("verilog_filename", &self.verilog_filename)
164 .field("manifest_filename", &self.manifest_filename)
165 .field("profile", &self.profile)
166 .field("top_module", &self.top_module)
167 .field("description", &self.description)
168 .field("validation_scope", &self.validation_scope)
169 .field("default_unit_generation", &self.default_unit_generation)
170 .finish_non_exhaustive()
171 }
172}
173
174fn reject_standalone_signer_generation() -> Result<String> {
175 bail!(
176 "the modular production signer has multiple immutable sources; use `rhdl-gen check --signer-full --keep`"
177 )
178}
179
180static SIGNER_FULL_TARGET: TargetSpec = TargetSpec {
181 name: full_signer_check::SIGNER_TARGET_NAME,
182 verilog_filename: "wots_sha256_signer_top.bundle.v",
183 manifest_filename: "wots_sha256_signer_top.bundle.manifest.json",
184 profile: "HASHSIGS_SHA256_GENERIC_V1",
185 top_module: full_signer_check::SIGNER_TOP_MODULE,
186 description: "modular complete SHA-256 three-cluster signer bundle",
187 validation_scope: "opt-in structural validation and finite generated-RTL equivalence only",
188 default_unit_generation: false,
189 generate: reject_standalone_signer_generation,
190};
191
192fn generate_keccak_round() -> Result<String> {
193 keccak_rhdl::generate_round_verilog().context("lowering the isolated Keccak round")
194}
195
196fn generate_keccak_permutation() -> Result<String> {
197 keccak_rhdl::generate_permutation_verilog()
198 .context("lowering the complete Keccak permutation oracle")
199}
200
201fn generate_keccak_iterative() -> Result<String> {
202 keccak_rhdl::generate_iterative_verilog().context("lowering the iterative Keccak engine")
203}
204
205fn combinational_kernel_module(
206 module_name: &str,
207 ports: &str,
208 declarations: &str,
209 function: &str,
210 assignment: &str,
211) -> String {
212 format!(
213 "module {module_name}({ports});\n{declarations}\n\n{function}\n\n{assignment}\nendmodule\n"
214 )
215}
216
217fn generate_sha256_round() -> Result<String> {
218 let function = sha256_rhdl::round_verilog().context("lowering the isolated SHA-256 round")?;
219 Ok(combinational_kernel_module(
220 "sha256_round_ooc",
221 "state_i, round_constant_i, state_o",
222 " input wire [767:0] state_i;\n input wire [31:0] round_constant_i;\n output wire [767:0] state_o;",
223 &function,
224 " assign state_o = kernel_round_kernel(state_i, round_constant_i);",
225 ))
226}
227
228fn generate_sha256_compression() -> Result<String> {
229 let function = sha256_rhdl::compression_verilog()
230 .context("lowering the complete combinational SHA-256 compression oracle")?;
231 Ok(combinational_kernel_module(
232 "sha256_compression_ooc",
233 "chaining_i, block_i, digest_o",
234 " input wire [255:0] chaining_i;\n input wire [511:0] block_i;\n output wire [255:0] digest_o;",
235 &function,
236 " assign digest_o = kernel_compression_kernel(chaining_i, block_i);",
237 ))
238}
239
240fn generate_sha256_pipeline_2() -> Result<String> {
241 sha256_rhdl::round_pipeline_verilog::<2>()
242 .context("lowering the checked two-stage SHA-256 round pipeline")
243}
244
245fn generate_sha256_pipeline_64_unchecked() -> Result<String> {
246 sha256_rhdl::round_pipeline_unchecked_verilog::<64>()
247 .context("rendering the unchecked 64-stage SHA-256 round pipeline")
248}
249
250fn generate_sha256_compression_lane_64_unchecked() -> Result<String> {
251 sha256_rhdl::lane::try_full_compression_lane_unchecked_verilog()
252 .context("rendering the unchecked complete 64-round SHA-256 compression lane")
253}
254
255fn generate_sha256_compression_farm_3x64_unchecked() -> Result<String> {
256 sha256_rhdl::farm::try_full_three_lane_farm_generation_only_unchecked_verilog()
257 .context("rendering the unchecked exact three-lane SHA-256 compression farm")
258}
259
260pub static TARGETS: &[TargetSpec] = &[
265 TargetSpec {
266 name: "legacy-keccak-round",
267 verilog_filename: "legacy_keccak_round.v",
268 manifest_filename: "legacy_keccak_round.manifest.json",
269 profile: "LEGACY_KECCAK",
270 top_module: "keccak_round_ooc",
271 description: "compile-time round-0 cell for isolated RTL evaluation",
272 validation_scope: "Icarus syntax/elaboration in the default check; no equivalence claim",
273 default_unit_generation: true,
274 generate: generate_keccak_round,
275 },
276 TargetSpec {
277 name: "legacy-keccak-permutation",
278 verilog_filename: "legacy_keccak_permutation.v",
279 manifest_filename: "legacy_keccak_permutation.manifest.json",
280 profile: "LEGACY_KECCAK",
281 top_module: "keccak_f1600_ooc",
282 description: "24-round combinational correctness oracle",
283 validation_scope: "export-only in the default check; no synthesis or hardware claim",
284 default_unit_generation: true,
285 generate: generate_keccak_permutation,
286 },
287 TargetSpec {
288 name: "legacy-keccak-iterative",
289 verilog_filename: "legacy_keccak_iterative.v",
290 manifest_filename: "legacy_keccak_iterative.manifest.json",
291 profile: "LEGACY_KECCAK",
292 top_module: "keccak_iterative",
293 description: "one-round-per-cycle 24-round engine",
294 validation_scope: "Icarus syntax/elaboration in the default check; no equivalence claim",
295 default_unit_generation: true,
296 generate: generate_keccak_iterative,
297 },
298 TargetSpec {
299 name: "sha256-round",
300 verilog_filename: "sha256_round.v",
301 manifest_filename: "sha256_round.manifest.json",
302 profile: "HASHSIGS_SHA256_GENERIC_V1",
303 top_module: "sha256_round_ooc",
304 description: "isolated combinational SHA-256 round with an explicit round constant",
305 validation_scope: "Icarus syntax/elaboration in the default check; no equivalence claim",
306 default_unit_generation: true,
307 generate: generate_sha256_round,
308 },
309 TargetSpec {
310 name: "sha256-compression-oracle",
311 verilog_filename: "sha256_compression_oracle.v",
312 manifest_filename: "sha256_compression_oracle.manifest.json",
313 profile: "HASHSIGS_SHA256_GENERIC_V1",
314 top_module: "sha256_compression_ooc",
315 description: "slow full combinational SHA-256 compression oracle",
316 validation_scope: "slow export-only target; excluded from default generation and Icarus checks",
317 default_unit_generation: false,
318 generate: generate_sha256_compression,
319 },
320 TargetSpec {
321 name: "sha256-pipeline-2",
322 verilog_filename: "sha256_pipeline_2.v",
323 manifest_filename: "sha256_pipeline_2.manifest.json",
324 profile: "HASHSIGS_SHA256_GENERIC_V1",
325 top_module: "sha256_round_pipeline",
326 description: "checked two-stage registered SHA-256 round-pipeline prefix",
327 validation_scope: "RHDL-checked generation plus Icarus syntax/elaboration in the default check",
328 default_unit_generation: true,
329 generate: generate_sha256_pipeline_2,
330 },
331 TargetSpec {
332 name: "sha256-pipeline-64-unchecked",
333 verilog_filename: "sha256_pipeline_64_unchecked.v",
334 manifest_filename: "sha256_pipeline_64_unchecked.manifest.json",
335 profile: "HASHSIGS_SHA256_GENERIC_V1",
336 top_module: "sha256_round_pipeline",
337 description: "unchecked rendering of the complete 64-stage SHA-256 round pipeline; not equivalence evidence",
338 validation_scope: "unchecked RHDL rendering; Verilator lint/control-only simulation are not RHDL/RTL equivalence evidence",
339 default_unit_generation: false,
340 generate: generate_sha256_pipeline_64_unchecked,
341 },
342 TargetSpec {
343 name: "sha256-compression-lane-64-unchecked",
344 verilog_filename: "sha256_compression_lane_64_unchecked.v",
345 manifest_filename: "sha256_compression_lane_64_unchecked.manifest.json",
346 profile: "HASHSIGS_SHA256_GENERIC_V1",
347 top_module: "sha256_compression_lane",
348 description: "complete 64-round BRAM-backed SHA-256 compression lane",
349 validation_scope: "unchecked RHDL rendering; `check --full` adds Verilator lint and arithmetic simulation, but no synthesis, timing, route, or hardware claim",
350 default_unit_generation: false,
351 generate: generate_sha256_compression_lane_64_unchecked,
352 },
353 TargetSpec {
354 name: "sha256-compression-farm-3x64-unchecked",
355 verilog_filename: "sha256_compression_farm_3x64_unchecked.v",
356 manifest_filename: "sha256_compression_farm_3x64_unchecked.manifest.json",
357 profile: "HASHSIGS_SHA256_GENERIC_V1",
358 top_module: "sha256_compression_farm_3x64",
359 description: "exact ThreeLaneCompressionFarm with independent complete SHA-256 lanes",
360 validation_scope: "unchecked RHDL rendering; `check --full` adds Verilator lint and cross-lane arithmetic simulation, but no synthesis, timing, route, or hardware claim",
361 default_unit_generation: false,
362 generate: generate_sha256_compression_farm_3x64_unchecked,
363 },
364 TargetSpec {
365 name: u280_hbm::U280_HBM_TARGET_NAME,
366 verilog_filename: "hashsigs_sha256_hbm_kernel.v",
367 manifest_filename: "hashsigs_sha256_hbm_kernel.manifest.json",
368 profile: "HASHSIGS_SHA256_GENERIC_V1",
369 top_module: u280_hbm::U280_HBM_TOP_MODULE,
370 description: "Rust/RHDL U280 HBM transport shell with an unresolved modular signer",
371 validation_scope: "metadata coverage and wire-only adapter validation; generation alone is not parse, simulation, synthesis, timing, route, resource, or hardware evidence",
372 default_unit_generation: false,
373 generate: u280_hbm::generate,
374 },
375];
376
377#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
379pub struct SourceProvenance {
380 pub commit: String,
382 pub dirty: bool,
384}
385
386impl SourceProvenance {
387 pub fn discover() -> Result<Self> {
395 let source_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
396 let commit = git_output(source_dir, ["rev-parse", "HEAD"])?;
397 let status = git_output(
398 source_dir,
399 ["status", "--porcelain=v1", "--untracked-files=normal"],
400 )?;
401 Ok(Self {
402 commit,
403 dirty: !status.is_empty(),
404 })
405 }
406}
407
408#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct ArtifactManifest {
412 pub schema_version: u32,
414 pub target: String,
416 pub profile: String,
418 pub evidence_tier: String,
420 pub validation_scope: String,
422 pub verilog_file: String,
424 pub verilog_sha256: String,
426 pub source_git_commit: String,
428 pub source_git_dirty: bool,
430 pub rhdl_revision: String,
432 pub hashsigs_revision: String,
434 pub generator_version: String,
436}
437
438#[derive(Clone, Debug, Eq, PartialEq)]
440pub struct GeneratedArtifact {
441 pub verilog_path: PathBuf,
443 pub manifest_path: PathBuf,
445 pub manifest: ArtifactManifest,
447}
448
449pub fn parse_args<I, S>(args: I) -> Result<Command>
455where
456 I: IntoIterator<Item = S>,
457 S: Into<OsString>,
458{
459 let mut args = args.into_iter().map(Into::into);
460 let Some(operation) = args.next() else {
461 bail!("missing command");
462 };
463 match operation.to_str() {
464 Some("list") => {
465 ensure_no_extra(args, "list")?;
466 Ok(Command::List)
467 }
468 Some("generate") => parse_generate(args),
469 Some("check") => parse_check(args),
470 Some("signer-throughput") => parse_signer_throughput(args),
471 Some("benchmark-kernel") => parse_benchmark_kernel(args),
472 Some("u280-link-graph") => parse_u280_link_graph(args),
473 Some("u280-verify-link-graph") => parse_u280_verify_link_graph(args),
474 Some("u280-hbm-rtl-check") => parse_u280_hbm_rtl_check(args),
475 Some("__signer-lower") => parse_internal_signer_lower(args),
476 Some("help" | "--help" | "-h") => {
477 ensure_no_extra(args, "help")?;
478 Ok(Command::Help)
479 }
480 Some(other) => bail!("unknown command `{other}`"),
481 None => bail!("command is not valid UTF-8"),
482 }
483}
484
485fn parse_benchmark_kernel(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
486 if args.next().as_deref() != Some(std::ffi::OsStr::new("--output")) {
487 bail!("benchmark-kernel requires --output <NEW_DIRECTORY>");
488 }
489 let output = PathBuf::from(
490 args.next()
491 .ok_or_else(|| anyhow!("benchmark-kernel requires --output <NEW_DIRECTORY>"))?,
492 );
493 ensure_no_extra(args, "benchmark-kernel")?;
494 Ok(Command::BenchmarkKernel { output })
495}
496
497fn parse_u280_link_graph(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
498 let mut kernel = None;
499 let mut kernel_manifest = None;
500 let mut signer_evidence = None;
501 let mut output = None;
502 while let Some(argument) = args.next() {
503 let (slot, description) = match argument.to_str() {
504 Some("--kernel") => (&mut kernel, "--kernel requires a Verilog file"),
505 Some("--kernel-manifest") => (
506 &mut kernel_manifest,
507 "--kernel-manifest requires a JSON file",
508 ),
509 Some("--signer-evidence") => (
510 &mut signer_evidence,
511 "--signer-evidence requires a directory",
512 ),
513 Some("--output") => (&mut output, "--output requires a new JSON file"),
514 Some(other) => bail!("unknown u280-link-graph argument `{other}`"),
515 None => bail!("u280-link-graph option is not valid UTF-8"),
516 };
517 if slot.is_some() {
518 bail!("{} may only be specified once", argument.to_string_lossy());
519 }
520 *slot = Some(PathBuf::from(
521 args.next().ok_or_else(|| anyhow!(description))?,
522 ));
523 }
524 Ok(Command::U280LinkGraph {
525 kernel: kernel.ok_or_else(|| anyhow!("u280-link-graph requires --kernel <VERILOG>"))?,
526 kernel_manifest: kernel_manifest
527 .ok_or_else(|| anyhow!("u280-link-graph requires --kernel-manifest <JSON>"))?,
528 signer_evidence: signer_evidence
529 .ok_or_else(|| anyhow!("u280-link-graph requires --signer-evidence <DIRECTORY>"))?,
530 output: output.ok_or_else(|| anyhow!("u280-link-graph requires --output <NEW_JSON>"))?,
531 })
532}
533
534fn parse_u280_verify_link_graph(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
535 let mut kernel = None;
536 let mut kernel_manifest = None;
537 let mut signer_evidence = None;
538 let mut record = None;
539 while let Some(argument) = args.next() {
540 let (slot, description) = match argument.to_str() {
541 Some("--kernel") => (&mut kernel, "--kernel requires a Verilog file"),
542 Some("--kernel-manifest") => (
543 &mut kernel_manifest,
544 "--kernel-manifest requires a JSON file",
545 ),
546 Some("--signer-evidence") => (
547 &mut signer_evidence,
548 "--signer-evidence requires a directory",
549 ),
550 Some("--record") => (&mut record, "--record requires an existing JSON file"),
551 Some(other) => bail!("unknown u280-verify-link-graph argument `{other}`"),
552 None => bail!("u280-verify-link-graph option is not valid UTF-8"),
553 };
554 if slot.is_some() {
555 bail!("{} may only be specified once", argument.to_string_lossy());
556 }
557 *slot = Some(PathBuf::from(
558 args.next().ok_or_else(|| anyhow!(description))?,
559 ));
560 }
561 Ok(Command::U280VerifyLinkGraph {
562 kernel: kernel
563 .ok_or_else(|| anyhow!("u280-verify-link-graph requires --kernel <VERILOG>"))?,
564 kernel_manifest: kernel_manifest
565 .ok_or_else(|| anyhow!("u280-verify-link-graph requires --kernel-manifest <JSON>"))?,
566 signer_evidence: signer_evidence.ok_or_else(|| {
567 anyhow!("u280-verify-link-graph requires --signer-evidence <DIRECTORY>")
568 })?,
569 record: record.ok_or_else(|| anyhow!("u280-verify-link-graph requires --record <JSON>"))?,
570 })
571}
572
573fn parse_u280_hbm_rtl_check(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
574 let mut kernel = None;
575 let mut kernel_manifest = None;
576 let mut signer_evidence = None;
577 let mut record = None;
578 let mut output = None;
579 while let Some(argument) = args.next() {
580 let (slot, description) = match argument.to_str() {
581 Some("--kernel") => (&mut kernel, "--kernel requires a Verilog file"),
582 Some("--kernel-manifest") => (
583 &mut kernel_manifest,
584 "--kernel-manifest requires a JSON file",
585 ),
586 Some("--signer-evidence") => (
587 &mut signer_evidence,
588 "--signer-evidence requires a directory",
589 ),
590 Some("--record") => (&mut record, "--record requires an existing JSON file"),
591 Some("--output") => (&mut output, "--output requires a new directory"),
592 Some(other) => bail!("unknown u280-hbm-rtl-check argument `{other}`"),
593 None => bail!("u280-hbm-rtl-check option is not valid UTF-8"),
594 };
595 if slot.is_some() {
596 bail!("{} may only be specified once", argument.to_string_lossy());
597 }
598 *slot = Some(PathBuf::from(
599 args.next().ok_or_else(|| anyhow!(description))?,
600 ));
601 }
602 Ok(Command::U280HbmRtlCheck {
603 kernel: kernel.ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --kernel <VERILOG>"))?,
604 kernel_manifest: kernel_manifest
605 .ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --kernel-manifest <JSON>"))?,
606 signer_evidence: signer_evidence
607 .ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --signer-evidence <DIRECTORY>"))?,
608 record: record.ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --record <JSON>"))?,
609 output: output
610 .ok_or_else(|| anyhow!("u280-hbm-rtl-check requires --output <NEW_DIRECTORY>"))?,
611 })
612}
613
614fn parse_signer_throughput(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
615 let mut source_evidence = None;
616 let mut output = None;
617 while let Some(argument) = args.next() {
618 match argument.to_str() {
619 Some("--source-evidence") => {
620 if source_evidence.is_some() {
621 bail!("--source-evidence may only be specified once");
622 }
623 source_evidence =
624 Some(PathBuf::from(args.next().ok_or_else(|| {
625 anyhow!("--source-evidence requires a directory")
626 })?));
627 }
628 Some("--output" | "-o") => {
629 if output.is_some() {
630 bail!("--output may only be specified once");
631 }
632 output =
633 Some(PathBuf::from(args.next().ok_or_else(|| {
634 anyhow!("--output requires a new directory")
635 })?));
636 }
637 Some(other) => bail!("unknown signer-throughput argument `{other}`"),
638 None => bail!("signer-throughput option is not valid UTF-8"),
639 }
640 }
641 Ok(Command::SignerThroughput {
642 source_evidence: source_evidence
643 .ok_or_else(|| anyhow!("signer-throughput requires --source-evidence <DIRECTORY>"))?,
644 output: output
645 .ok_or_else(|| anyhow!("signer-throughput requires --output <NEW_DIRECTORY>"))?,
646 })
647}
648
649fn parse_internal_signer_lower(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
650 let kind = required_utf8(args.next(), "internal signer lowering kind")?;
651 if !matches!(kind.as_str(), "lane" | "skeleton" | "benchmark-skeleton") {
652 bail!("unknown internal signer lowering kind `{kind}`");
653 }
654 if args.next().as_deref() != Some(std::ffi::OsStr::new("--output")) {
655 bail!("internal signer lowerer requires --output <FILE>");
656 }
657 let output = PathBuf::from(
658 args.next()
659 .ok_or_else(|| anyhow!("internal signer lowerer requires an output file"))?,
660 );
661 if args.next().as_deref() != Some(std::ffi::OsStr::new("--source-commit")) {
662 bail!("internal signer lowerer requires --source-commit <COMMIT>");
663 }
664 let source_commit = required_utf8(args.next(), "internal signer source commit")?;
665 ensure_no_extra(args, "internal signer lowerer")?;
666 Ok(Command::InternalSignerLower {
667 kind,
668 output,
669 source_commit,
670 })
671}
672
673fn parse_generate(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
674 let target = required_utf8(args.next(), "generation target")?;
675 let mut output = None;
676 let mut force = false;
677 while let Some(argument) = args.next() {
678 match argument.to_str() {
679 Some("--output" | "-o") => {
680 if output.is_some() {
681 bail!("--output may only be specified once");
682 }
683 output = Some(PathBuf::from(
684 args.next()
685 .ok_or_else(|| anyhow!("--output requires a directory"))?,
686 ));
687 }
688 Some("--force") if !force => force = true,
689 Some("--force") => bail!("--force may only be specified once"),
690 Some(other) => bail!("unknown generate argument `{other}`"),
691 None => bail!("generate option is not valid UTF-8"),
692 }
693 }
694 Ok(Command::Generate {
695 target,
696 output: output.ok_or_else(|| anyhow!("generate requires --output <DIRECTORY>"))?,
697 force,
698 })
699}
700
701fn parse_check(mut args: impl Iterator<Item = OsString>) -> Result<Command> {
702 let mut temp_root = None;
703 let mut keep = false;
704 let mut full = false;
705 let mut signer_full = false;
706 while let Some(argument) = args.next() {
707 match argument.to_str() {
708 Some("--temp-root") => {
709 if temp_root.is_some() {
710 bail!("--temp-root may only be specified once");
711 }
712 temp_root =
713 Some(PathBuf::from(args.next().ok_or_else(|| {
714 anyhow!("--temp-root requires a directory")
715 })?));
716 }
717 Some("--keep") if !keep => keep = true,
718 Some("--keep") => bail!("--keep may only be specified once"),
719 Some("--full") if !full => full = true,
720 Some("--full") => bail!("--full may only be specified once"),
721 Some("--signer-full") if !signer_full => signer_full = true,
722 Some("--signer-full") => bail!("--signer-full may only be specified once"),
723 Some(other) => bail!("unknown check argument `{other}`"),
724 None => bail!("check option is not valid UTF-8"),
725 }
726 }
727 Ok(Command::Check {
728 temp_root,
729 keep,
730 full,
731 signer_full,
732 })
733}
734
735fn required_utf8(value: Option<OsString>, description: &str) -> Result<String> {
736 let value = value.ok_or_else(|| anyhow!("missing {description}"))?;
737 value
738 .into_string()
739 .map_err(|_| anyhow!("{description} is not valid UTF-8"))
740}
741
742fn ensure_no_extra(mut args: impl Iterator<Item = OsString>, operation: &str) -> Result<()> {
743 if let Some(extra) = args.next() {
744 bail!(
745 "{operation} does not accept argument `{}`",
746 extra.to_string_lossy()
747 );
748 }
749 Ok(())
750}
751
752pub fn run_from_args<I, S>(args: I) -> Result<String>
759where
760 I: IntoIterator<Item = S>,
761 S: Into<OsString>,
762{
763 execute(parse_args(args)?)
764}
765
766pub fn execute(command: Command) -> Result<String> {
772 match command {
773 Command::List => Ok(render_target_list()),
774 Command::Help => Ok(USAGE.to_owned()),
775 Command::Generate {
776 target,
777 output,
778 force,
779 } => {
780 let spec = find_target(&target)?;
781 let provenance = SourceProvenance::discover()?;
782 let artifact = generate_target(spec, &output, force, &provenance)?;
783 Ok(format!(
784 "generated {}\nmanifest {}\nevidence tier: {}",
785 artifact.verilog_path.display(),
786 artifact.manifest_path.display(),
787 GENERATED_VERILOG_EVIDENCE_TIER
788 ))
789 }
790 Command::Check {
791 temp_root,
792 keep,
793 full,
794 signer_full,
795 } => run_check(temp_root.as_deref(), keep, full, signer_full),
796 Command::SignerThroughput {
797 source_evidence,
798 output,
799 } => run_signer_throughput_check(&source_evidence, &output),
800 Command::BenchmarkKernel { output } => {
801 benchmark_kernel::run_benchmark_kernel_generation(&output)
802 }
803 Command::U280LinkGraph {
804 kernel,
805 kernel_manifest,
806 signer_evidence,
807 output,
808 } => {
809 let record = u280_hbm_check::write_immutable_combined_link_record(
810 &kernel,
811 &kernel_manifest,
812 &signer_evidence,
813 &output,
814 )?;
815 Ok(format!(
816 "validated immutable provenance-bound U280 link graph\nrecord {}\nkernel sha256 {}\nsigner sha256 {}\nreachable modules {}\nevidence boundary: exact current generated source, successful finite signer evidence, and structural reachability only; not synthesis, timing, route, resource, or hardware evidence",
817 output.display(),
818 record.kernel.sha256,
819 record.signer_bundle.sha256,
820 record.modules.len()
821 ))
822 }
823 Command::U280VerifyLinkGraph {
824 kernel,
825 kernel_manifest,
826 signer_evidence,
827 record,
828 } => {
829 let validated = u280_hbm_check::verify_immutable_combined_link_record(
830 &kernel,
831 &kernel_manifest,
832 &signer_evidence,
833 &record,
834 )?;
835 Ok(format!(
836 "verified immutable provenance-bound U280 link graph\nrecord {}\nkernel sha256 {}\nsigner sha256 {}\nreachable modules {}\nno files written",
837 record.display(),
838 validated.kernel.sha256,
839 validated.signer_bundle.sha256,
840 validated.modules.len()
841 ))
842 }
843 Command::U280HbmRtlCheck {
844 kernel,
845 kernel_manifest,
846 signer_evidence,
847 record,
848 output,
849 } => u280_hbm_rtl_check::run(
850 &kernel,
851 &kernel_manifest,
852 &signer_evidence,
853 &record,
854 &output,
855 ),
856 Command::InternalSignerLower {
857 kind,
858 output,
859 source_commit,
860 } => full_signer_check::run_internal_lowerer(&kind, &output, &source_commit),
861 }
862}
863
864pub fn find_target(name: &str) -> Result<&'static TargetSpec> {
870 TARGETS
871 .iter()
872 .find(|target| target.name == name)
873 .ok_or_else(|| {
874 anyhow!(
875 "unknown target `{name}`; available targets: {}",
876 TARGETS
877 .iter()
878 .map(|target| target.name)
879 .collect::<Vec<_>>()
880 .join(", ")
881 )
882 })
883}
884
885fn render_target_list() -> String {
886 let mut output = String::from("TARGET\tPROFILE\tTOP\tVALIDATION\tDESCRIPTION\n");
887 for target in TARGETS {
888 let _ = writeln!(
889 output,
890 "{}\t{}\t{}\t{}\t{}",
891 target.name,
892 target.profile,
893 target.top_module,
894 target.validation_scope,
895 target.description
896 );
897 }
898 output.pop();
899 output
900}
901
902pub fn generate_target(
913 target: &TargetSpec,
914 output_dir: &Path,
915 force: bool,
916 provenance: &SourceProvenance,
917) -> Result<GeneratedArtifact> {
918 fs::create_dir_all(output_dir).with_context(|| {
919 format!(
920 "creating selected output directory {}",
921 output_dir.display()
922 )
923 })?;
924 let verilog_path = output_dir.join(target.verilog_filename);
925 let manifest_path = output_dir.join(target.manifest_filename);
926 check_overwrite(&verilog_path, force)?;
927 check_overwrite(&manifest_path, force)?;
928
929 let mut verilog = (target.generate)()?;
930 if !verilog.ends_with('\n') {
931 verilog.push('\n');
932 }
933 let digest = hex::encode(Sha256::digest(verilog.as_bytes()));
934 let manifest = ArtifactManifest {
935 schema_version: 2,
936 target: target.name.to_owned(),
937 profile: target.profile.to_owned(),
938 evidence_tier: GENERATED_VERILOG_EVIDENCE_TIER.to_owned(),
939 validation_scope: target.validation_scope.to_owned(),
940 verilog_file: target.verilog_filename.to_owned(),
941 verilog_sha256: digest,
942 source_git_commit: provenance.commit.clone(),
943 source_git_dirty: provenance.dirty,
944 rhdl_revision: RHDL_REVISION.to_owned(),
945 hashsigs_revision: HASHSIGS_REVISION.to_owned(),
946 generator_version: env!("CARGO_PKG_VERSION").to_owned(),
947 };
948 let mut manifest_json =
949 serde_json::to_string_pretty(&manifest).context("serializing the provenance manifest")?;
950 manifest_json.push('\n');
951
952 write_output(&verilog_path, verilog.as_bytes(), force)?;
953 if let Err(error) = write_output(&manifest_path, manifest_json.as_bytes(), force) {
954 if !force {
955 let _ = fs::remove_file(&verilog_path);
956 }
957 return Err(error);
958 }
959 Ok(GeneratedArtifact {
960 verilog_path,
961 manifest_path,
962 manifest,
963 })
964}
965
966fn check_overwrite(path: &Path, force: bool) -> Result<()> {
967 if path.exists() && !force {
968 bail!(
969 "refusing to overwrite {}; pass --force to replace this target's files",
970 path.display()
971 );
972 }
973 Ok(())
974}
975
976fn write_output(path: &Path, content: &[u8], force: bool) -> Result<()> {
977 let mut options = OpenOptions::new();
978 options.write(true);
979 if force {
980 options.create(true).truncate(true);
981 } else {
982 options.create_new(true);
983 }
984 let mut file = options
985 .open(path)
986 .with_context(|| format!("opening output {}", path.display()))?;
987 file.write_all(content)
988 .with_context(|| format!("writing output {}", path.display()))?;
989 file.sync_all()
990 .with_context(|| format!("syncing output {}", path.display()))
991}
992
993fn git_output<const N: usize>(source_dir: &Path, arguments: [&str; N]) -> Result<String> {
994 let output = ProcessCommand::new("git")
995 .env("GIT_OPTIONAL_LOCKS", "0")
996 .arg("--no-optional-locks")
997 .arg("-C")
998 .arg(source_dir)
999 .args(arguments)
1000 .output()
1001 .context("running Git for source provenance")?;
1002 if !output.status.success() {
1003 bail!(
1004 "Git source-provenance command failed: {}",
1005 String::from_utf8_lossy(&output.stderr).trim()
1006 );
1007 }
1008 String::from_utf8(output.stdout)
1009 .context("Git source-provenance output is not UTF-8")
1010 .map(|output| output.trim().to_owned())
1011}
1012
1013fn run_check(
1014 temp_root: Option<&Path>,
1015 keep: bool,
1016 full: bool,
1017 signer_full: bool,
1018) -> Result<String> {
1019 let provenance = SourceProvenance::discover()?;
1020 if signer_full && provenance.dirty {
1021 bail!(
1022 "`rhdl-gen check --signer-full` requires a clean source worktree; commit or remove source changes before producing immutable RTL evidence"
1023 );
1024 }
1025 let root = temp_root.map_or_else(std::env::temp_dir, Path::to_path_buf);
1026 fs::create_dir_all(&root)
1027 .with_context(|| format!("creating selected temporary root {}", root.display()))?;
1028 let workspace = CheckWorkspace::create(&root, keep)?;
1029 require_iverilog(workspace.path())?;
1030 let quick_targets = [
1031 "legacy-keccak-round",
1032 "legacy-keccak-iterative",
1033 "sha256-round",
1034 "sha256-pipeline-2",
1035 ];
1036 for target_name in quick_targets {
1037 let target = find_target(target_name)?;
1038 let artifact = generate_target(target, workspace.path(), false, &provenance)?;
1039 elaborate_with_iverilog(target, &artifact.verilog_path, workspace.path())?;
1040 }
1041 let full_report = if full {
1042 let full_targets = [
1043 (
1044 "sha256-compression-lane-64-unchecked",
1045 "sha256_lane_arithmetic_tb",
1046 full_sha_check::lane_testbench(),
1047 full_sha_check::LANE_PASS_MARKER,
1048 ),
1049 (
1050 "sha256-compression-farm-3x64-unchecked",
1051 "sha256_farm_arithmetic_tb",
1052 full_sha_check::farm_testbench(),
1053 full_sha_check::FARM_PASS_MARKER,
1054 ),
1055 ];
1056 let mut reports = Vec::with_capacity(full_targets.len() * 2);
1057 for (target_name, testbench_top, testbench, pass_marker) in full_targets {
1058 let target = find_target(target_name)?;
1059 let artifact = generate_target(target, workspace.path(), false, &provenance)?;
1060 reports.push(lint_with_verilator(
1061 target,
1062 &artifact.verilog_path,
1063 workspace.path(),
1064 keep,
1065 )?);
1066 reports.push(simulate_sha_arithmetic_with_verilator(
1067 target,
1068 &artifact.verilog_path,
1069 workspace.path(),
1070 keep,
1071 testbench_top,
1072 &testbench,
1073 pass_marker,
1074 )?);
1075 }
1076 Some(reports.join("\n"))
1077 } else {
1078 None
1079 };
1080 let signer_report = if signer_full {
1081 Some(run_signer_full_check(workspace.path(), keep, &provenance)?)
1082 } else {
1083 None
1084 };
1085 let disposition = if keep {
1086 format!("kept at {}", workspace.path().display())
1087 } else {
1088 "removed after success".to_owned()
1089 };
1090 let evidence_tier = GENERATED_VERILOG_EVIDENCE_TIER;
1091 let mut report = format!(
1092 "Icarus syntax/elaboration checks passed for {}\nevidence boundary: syntax/elaboration only; not simulation or equivalence evidence",
1093 quick_targets.join(", ")
1094 );
1095 if let Some(full_report) = full_report {
1096 let _ = write!(report, "\n{full_report}");
1097 }
1098 if let Some(signer_report) = signer_report {
1099 let _ = write!(report, "\n{signer_report}");
1100 }
1101 let _ = write!(
1102 report,
1103 "\ntemporary workspace: {disposition}\nevidence tier: {evidence_tier}"
1104 );
1105 Ok(report)
1106}
1107
1108fn run_signer_full_check(
1109 output_dir: &Path,
1110 keep: bool,
1111 provenance: &SourceProvenance,
1112) -> Result<String> {
1113 let artifact = full_signer_check::generate_bundle(output_dir, provenance)?;
1114 full_signer_check::validate_persisted_bundle(&artifact)?;
1115 let lint = lint_with_verilator(&SIGNER_FULL_TARGET, &artifact.bundle_path, output_dir, keep)?;
1116 let lint_log_path = output_dir.join(format!(
1117 "verilator-lint-{}.log",
1118 full_signer_check::SIGNER_TARGET_NAME
1119 ));
1120 let object_dir = output_dir.join(format!(
1121 "verilator_sim_{}",
1122 full_signer_check::SIGNER_TARGET_NAME
1123 ));
1124
1125 full_signer_check::validate_persisted_bundle(&artifact)?;
1126 let build = build_verilator_testbench(VerilatorBuildRequest {
1127 target: &SIGNER_FULL_TARGET,
1128 verilog: &artifact.bundle_path,
1129 output_dir,
1130 keep,
1131 testbench_top: full_signer_check::SIGNER_TESTBENCH_TOP,
1132 testbench_path: &artifact.testbench_path,
1133 object_dir: &object_dir,
1134 evidence_label: "signer",
1135 })?;
1136 full_signer_check::validate_persisted_bundle(&artifact)?;
1137 let simulation = run_verilator_testbench(
1138 &SIGNER_FULL_TARGET,
1139 output_dir,
1140 keep,
1141 full_signer_check::SIGNER_TESTBENCH_TOP,
1142 full_signer_check::SIGNER_PASS_MARKER,
1143 &object_dir,
1144 "signer",
1145 )?;
1146 full_signer_check::validate_persisted_bundle(&artifact)?;
1147 let evidence_manifest = full_signer_check::write_check_evidence_manifest(
1148 &artifact,
1149 &lint_log_path,
1150 &build.log_path,
1151 &simulation.log_path,
1152 )?;
1153
1154 Ok(format!(
1155 "{lint}\nVerilator modular production-signer simulation passed with marker `{}` after {:.3}s build + {:.3}s run\nevidence boundary: exact finite-vector generated-RTL equivalence for one reset-discarded job and twelve completed jobs, including saturation, frame bytes, job identity, ready/valid stability, final hold, releases, and quiescence; not formal proof, sustained-throughput, synthesis, timing, route, resource, or hardware evidence\nimmutable lane source: {}\nimmutable signer skeleton: {}\nimmutable linked bundle: {}\nimmutable bundle manifest: {}\nimmutable signer testbench: {}\nimmutable lowering logs: {}, {}\nimmutable check-evidence manifest: {}\nVerilator signer logs: {}, {}",
1156 full_signer_check::SIGNER_PASS_MARKER,
1157 build.elapsed.as_secs_f64(),
1158 simulation.elapsed.as_secs_f64(),
1159 render_artifact_location(&artifact.lane_path, keep),
1160 render_artifact_location(&artifact.skeleton_path, keep),
1161 render_artifact_location(&artifact.bundle_path, keep),
1162 render_artifact_location(&artifact.manifest_path, keep),
1163 render_artifact_location(&artifact.testbench_path, keep),
1164 render_artifact_location(&artifact.lane_lowering_log_path, keep),
1165 render_artifact_location(&artifact.skeleton_lowering_log_path, keep),
1166 render_artifact_location(&evidence_manifest, keep),
1167 render_artifact_location(&build.log_path, keep),
1168 render_artifact_location(&simulation.log_path, keep),
1169 ))
1170}
1171
1172fn create_throughput_output_directory(selected: &Path, source_evidence: &Path) -> Result<PathBuf> {
1173 ensure!(
1174 !selected.exists(),
1175 "signer-throughput output already exists; select a new directory: {}",
1176 selected.display()
1177 );
1178 let file_name = selected
1179 .file_name()
1180 .filter(|name| !name.is_empty())
1181 .ok_or_else(|| anyhow!("signer-throughput output must name a new child directory"))?;
1182 let parent = selected
1183 .parent()
1184 .filter(|parent| !parent.as_os_str().is_empty())
1185 .unwrap_or_else(|| Path::new("."));
1186 ensure!(
1187 parent.is_dir(),
1188 "signer-throughput output parent does not exist: {}",
1189 parent.display()
1190 );
1191 let parent = fs::canonicalize(parent)
1192 .with_context(|| format!("canonicalizing output parent {}", parent.display()))?;
1193 let output = parent.join(file_name);
1194 ensure!(
1195 !output.starts_with(source_evidence),
1196 "signer-throughput output must not modify the immutable source-evidence directory"
1197 );
1198 let repository = Path::new(env!("CARGO_MANIFEST_DIR"))
1199 .parent()
1200 .and_then(Path::parent)
1201 .context("rhdl-gen crate is not nested beneath the workspace root")?;
1202 let repository = fs::canonicalize(repository)
1203 .context("canonicalizing the signer-throughput flow workspace")?;
1204 ensure!(
1205 !output.starts_with(&repository),
1206 "signer-throughput output must be outside the source workspace"
1207 );
1208 fs::create_dir(&output)
1209 .with_context(|| format!("creating signer-throughput output {}", output.display()))?;
1210 fs::canonicalize(&output).with_context(|| {
1211 format!(
1212 "canonicalizing signer-throughput output {}",
1213 output.display()
1214 )
1215 })
1216}
1217
1218fn require_verilator_version(output_dir: &Path) -> Result<String> {
1219 let output = ProcessCommand::new("verilator")
1220 .env("TMPDIR", output_dir)
1221 .env("TMP", output_dir)
1222 .env("TEMP", output_dir)
1223 .arg("--version")
1224 .output()
1225 .map_err(|error| {
1226 if error.kind() == std::io::ErrorKind::NotFound {
1227 anyhow!(
1228 "Verilator is required for `rhdl-gen signer-throughput` but was not found in PATH"
1229 )
1230 } else {
1231 anyhow!(error)
1232 }
1233 })?;
1234 ensure!(
1235 output.status.success(),
1236 "Verilator is present but `verilator --version` failed: {}",
1237 String::from_utf8_lossy(&output.stderr).trim()
1238 );
1239 let version = String::from_utf8(output.stdout).context("Verilator version is not UTF-8")?;
1240 ensure!(
1241 !version.trim().is_empty(),
1242 "Verilator returned an empty version"
1243 );
1244 Ok(version.trim().to_owned())
1245}
1246
1247fn run_signer_throughput_check(source_evidence: &Path, output: &Path) -> Result<String> {
1248 let flow_provenance = SourceProvenance::discover()?;
1249 ensure!(
1250 !flow_provenance.dirty,
1251 "`rhdl-gen signer-throughput` requires a clean flow worktree so its evidence binds committed code"
1252 );
1253 let source = full_signer_check::validate_successful_evidence_directory(source_evidence)?;
1254 let output_dir = create_throughput_output_directory(output, &source.directory)?;
1255 let flow_executable = std::env::current_exe().context("locating the rhdl-gen executable")?;
1256 let flow_executable_record = full_signer_check::file_record(&flow_executable)?;
1257 let verilator_version = require_verilator_version(&output_dir)?;
1258 let testbench_path = signer_throughput_check::write_throughput_testbench(&output_dir)?;
1259 let testbench_record = full_signer_check::file_record(&testbench_path)?;
1260 let object_dir = output_dir.join("verilator_sim_sha256-signer-sustained-throughput");
1261
1262 ensure!(
1263 full_signer_check::validate_successful_evidence_directory(&source.directory)? == source,
1264 "signer source evidence changed before throughput build"
1265 );
1266 let build = build_verilator_testbench(VerilatorBuildRequest {
1267 target: &SIGNER_FULL_TARGET,
1268 verilog: &source.bundle_path,
1269 output_dir: &output_dir,
1270 keep: true,
1271 testbench_top: signer_throughput_check::THROUGHPUT_TESTBENCH_TOP,
1272 testbench_path: &testbench_path,
1273 object_dir: &object_dir,
1274 evidence_label: "signer-throughput",
1275 })?;
1276 ensure!(
1277 full_signer_check::file_record(&testbench_path)? == testbench_record,
1278 "throughput testbench changed during Verilator build"
1279 );
1280 ensure!(
1281 full_signer_check::validate_successful_evidence_directory(&source.directory)? == source,
1282 "signer source evidence changed during throughput build"
1283 );
1284 let simulator = object_dir.join(format!(
1285 "V{}",
1286 signer_throughput_check::THROUGHPUT_TESTBENCH_TOP
1287 ));
1288 let simulator_record = full_signer_check::file_record(&simulator)?;
1289 let simulation = run_verilator_testbench(
1290 &SIGNER_FULL_TARGET,
1291 &output_dir,
1292 true,
1293 signer_throughput_check::THROUGHPUT_TESTBENCH_TOP,
1294 signer_throughput_check::THROUGHPUT_PASS_MARKER,
1295 &object_dir,
1296 "signer-throughput",
1297 )?;
1298 ensure!(
1299 full_signer_check::file_record(&simulator)? == simulator_record,
1300 "Verilator throughput simulator changed while it was running"
1301 );
1302 ensure!(
1303 full_signer_check::file_record(&testbench_path)? == testbench_record,
1304 "throughput testbench changed during simulation"
1305 );
1306 ensure!(
1307 full_signer_check::validate_successful_evidence_directory(&source.directory)? == source,
1308 "signer source evidence changed during throughput simulation"
1309 );
1310 let final_flow_provenance = SourceProvenance::discover()?;
1311 ensure!(
1312 final_flow_provenance == flow_provenance && !final_flow_provenance.dirty,
1313 "throughput flow source changed during the external build or simulation"
1314 );
1315 ensure!(
1316 full_signer_check::file_record(&flow_executable)? == flow_executable_record,
1317 "rhdl-gen executable changed during the throughput gate"
1318 );
1319 let manifest = signer_throughput_check::write_throughput_manifest(
1320 &output_dir,
1321 &signer_throughput_check::ThroughputManifestInputs {
1322 source: &source,
1323 flow_provenance: &flow_provenance,
1324 flow_executable: &flow_executable,
1325 verilator_version: &verilator_version,
1326 testbench: &testbench_path,
1327 simulator: &simulator,
1328 build_log: &build.log_path,
1329 simulation_log: &simulation.log_path,
1330 },
1331 )?;
1332
1333 Ok(format!(
1334 "Verilator sustained signer throughput simulation passed with marker `{}` after {:.3}s build + {:.3}s run\nevidence boundary: 72 exact fused private-seed oracle frames under continuous admission and always-ready output; completion boundaries 12 through 60 must span at most 24,000 cycles (500 cycles/result normalized at 250 MHz); not formal proof, synthesis, timing, route, resource, or hardware evidence\nimmutable source signer bundle: {}\nimmutable source bundle/check-evidence manifests: {}, {}\nimmutable throughput testbench: {}\nread-only throughput simulator: {}\nimmutable throughput build/simulation logs: {}, {}\nimmutable throughput evidence manifest: {}",
1335 signer_throughput_check::THROUGHPUT_PASS_MARKER,
1336 build.elapsed.as_secs_f64(),
1337 simulation.elapsed.as_secs_f64(),
1338 source.bundle_path.display(),
1339 source.bundle_manifest_path.display(),
1340 source.check_evidence_manifest_path.display(),
1341 testbench_path.display(),
1342 simulator.display(),
1343 build.log_path.display(),
1344 simulation.log_path.display(),
1345 manifest.display(),
1346 ))
1347}
1348
1349fn lint_with_verilator(
1350 target: &TargetSpec,
1351 verilog: &Path,
1352 output_dir: &Path,
1353 keep: bool,
1354) -> Result<String> {
1355 let version = ProcessCommand::new("verilator")
1356 .env("TMPDIR", output_dir)
1357 .env("TMP", output_dir)
1358 .env("TEMP", output_dir)
1359 .arg("--version")
1360 .output()
1361 .map_err(|error| {
1362 if error.kind() == std::io::ErrorKind::NotFound {
1363 anyhow!(
1364 "Verilator is required for `rhdl-gen check --full` or `--signer-full` but was not found in PATH"
1365 )
1366 } else {
1367 anyhow!(error)
1368 }
1369 })?;
1370 if !version.status.success() {
1371 bail!(
1372 "Verilator is present but `verilator --version` failed: {}",
1373 String::from_utf8_lossy(&version.stderr).trim()
1374 );
1375 }
1376 let version = String::from_utf8_lossy(&version.stdout).trim().to_owned();
1377 let object_dir = output_dir.join(format!("verilator_lint_{}", target.name));
1378 let started = Instant::now();
1379 let output = ProcessCommand::new("verilator")
1380 .env("TMPDIR", output_dir)
1381 .env("TMP", output_dir)
1382 .env("TEMP", output_dir)
1383 .arg("--lint-only")
1384 .arg("-Wall")
1385 .arg("-Wno-fatal")
1386 .arg("--threads")
1387 .arg("1")
1388 .arg("--top-module")
1389 .arg(target.top_module)
1390 .arg("--Mdir")
1391 .arg(&object_dir)
1392 .arg(verilog)
1393 .output()
1394 .with_context(|| format!("invoking Verilator for target {}", target.name))?;
1395 let elapsed = started.elapsed();
1396 let stdout = String::from_utf8_lossy(&output.stdout);
1397 let stderr = String::from_utf8_lossy(&output.stderr);
1398 let warning_inventory = verilator_warning_inventory(&stderr);
1399 let warning_count = warning_inventory.values().sum::<usize>();
1400 let warning_summary = render_warning_inventory(&warning_inventory);
1401 let log_path = output_dir.join(format!("verilator-lint-{}.log", target.name));
1402 let log = format!(
1403 "command: verilator --lint-only -Wall -Wno-fatal --threads 1 --top-module {} --Mdir {} {}\nversion: {version}\nelapsed_seconds: {:.3}\nstatus: {}\nstdout:\n{}\nstderr:\n{}\n",
1404 target.top_module,
1405 object_dir.display(),
1406 verilog.display(),
1407 elapsed.as_secs_f64(),
1408 output.status,
1409 stdout.trim(),
1410 stderr.trim(),
1411 );
1412 fs::write(&log_path, log)
1413 .with_context(|| format!("writing Verilator diagnostic log {}", log_path.display()))?;
1414 if !output.status.success() {
1415 bail!(
1416 "Verilator lint/elaboration failed for target {} (top {}) after {:.3}s; diagnostic log: {}:\nstdout:\n{}\nstderr:\n{}",
1417 target.name,
1418 target.top_module,
1419 elapsed.as_secs_f64(),
1420 render_artifact_location(&log_path, keep),
1421 stdout.trim(),
1422 stderr.trim()
1423 );
1424 }
1425 Ok(format!(
1426 "Verilator lint/elaboration passed for {} (top {}) with {version} in {:.3}s; accepted warning inventory: {} total ({}) (captured in log)\nevidence boundary: unchecked rendering plus lint/elaboration only; not simulation or equivalence evidence; warning acceptance is not a warning-clean claim\nVerilator log: {}",
1427 target.name,
1428 target.top_module,
1429 elapsed.as_secs_f64(),
1430 warning_count,
1431 warning_summary,
1432 render_artifact_location(&log_path, keep)
1433 ))
1434}
1435
1436fn verilator_warning_inventory(stderr: &str) -> BTreeMap<String, usize> {
1437 let mut inventory = BTreeMap::new();
1438 for code in stderr.lines().filter_map(|line| {
1439 line.strip_prefix("%Warning-")
1440 .and_then(|warning| warning.split_once(':'))
1441 .map(|(code, _)| code)
1442 }) {
1443 *inventory.entry(code.to_owned()).or_default() += 1;
1444 }
1445 inventory
1446}
1447
1448fn render_warning_inventory(inventory: &BTreeMap<String, usize>) -> String {
1449 if inventory.is_empty() {
1450 return "none".to_owned();
1451 }
1452 let mut summary = String::new();
1453 for (index, (code, count)) in inventory.iter().enumerate() {
1454 if index != 0 {
1455 summary.push_str(", ");
1456 }
1457 write!(summary, "{code}={count}").expect("writing to a String cannot fail");
1458 }
1459 summary
1460}
1461
1462fn simulate_sha_arithmetic_with_verilator(
1463 target: &TargetSpec,
1464 verilog: &Path,
1465 output_dir: &Path,
1466 keep: bool,
1467 testbench_top: &str,
1468 testbench: &str,
1469 pass_marker: &str,
1470) -> Result<String> {
1471 let testbench_path = output_dir.join(format!("{testbench_top}.sv"));
1472 fs::write(&testbench_path, testbench).with_context(|| {
1473 format!(
1474 "writing SHA arithmetic testbench {}",
1475 testbench_path.display()
1476 )
1477 })?;
1478 let object_dir = output_dir.join(format!("verilator_sim_{}", target.name));
1479 let build = build_verilator_testbench(VerilatorBuildRequest {
1480 target,
1481 verilog,
1482 output_dir,
1483 keep,
1484 testbench_top,
1485 testbench_path: &testbench_path,
1486 object_dir: &object_dir,
1487 evidence_label: "arithmetic",
1488 })?;
1489 let simulation = run_verilator_testbench(
1490 target,
1491 output_dir,
1492 keep,
1493 testbench_top,
1494 pass_marker,
1495 &object_dir,
1496 "arithmetic",
1497 )?;
1498 Ok(format!(
1499 "Verilator generated-RTL arithmetic simulation passed for {} with marker `{}` after {:.3}s build + {:.3}s run\nevidence boundary: deterministic host-model equivalence, exact latency, tag, bubble, and reset-valid behavior for the exercised vectors only; not formal proof, synthesis, timing, route, or hardware evidence\nVerilator testbench: {}\nVerilator simulation logs: {}, {}",
1500 target.name,
1501 pass_marker,
1502 build.elapsed.as_secs_f64(),
1503 simulation.elapsed.as_secs_f64(),
1504 render_artifact_location(&testbench_path, keep),
1505 render_artifact_location(&build.log_path, keep),
1506 render_artifact_location(&simulation.log_path, keep)
1507 ))
1508}
1509
1510struct VerilatorPhaseEvidence {
1511 elapsed: Duration,
1512 log_path: PathBuf,
1513}
1514
1515#[derive(Clone, Copy)]
1516struct VerilatorBuildRequest<'a> {
1517 target: &'a TargetSpec,
1518 verilog: &'a Path,
1519 output_dir: &'a Path,
1520 keep: bool,
1521 testbench_top: &'a str,
1522 testbench_path: &'a Path,
1523 object_dir: &'a Path,
1524 evidence_label: &'a str,
1525}
1526
1527fn build_verilator_testbench(request: VerilatorBuildRequest<'_>) -> Result<VerilatorPhaseEvidence> {
1528 let VerilatorBuildRequest {
1529 target,
1530 verilog,
1531 output_dir,
1532 keep,
1533 testbench_top,
1534 testbench_path,
1535 object_dir,
1536 evidence_label,
1537 } = request;
1538 let build_started = Instant::now();
1539 let build = ProcessCommand::new("verilator")
1540 .env("TMPDIR", output_dir)
1541 .env("TMP", output_dir)
1542 .env("TEMP", output_dir)
1543 .arg("--binary")
1544 .arg("--timing")
1545 .arg("-Wno-fatal")
1546 .arg("-Wno-DECLFILENAME")
1547 .arg("-Wno-UNUSEDSIGNAL")
1548 .arg("--threads")
1549 .arg("1")
1550 .arg("-j")
1551 .arg("1")
1552 .arg("--top-module")
1553 .arg(testbench_top)
1554 .arg("--Mdir")
1555 .arg(object_dir)
1556 .arg(verilog)
1557 .arg(testbench_path)
1558 .output()
1559 .with_context(|| {
1560 format!(
1561 "building the Verilator {evidence_label} testbench for {}",
1562 target.name,
1563 )
1564 })?;
1565 let build_elapsed = build_started.elapsed();
1566 let build_stdout = String::from_utf8_lossy(&build.stdout);
1567 let build_stderr = String::from_utf8_lossy(&build.stderr);
1568 let build_log_path = output_dir.join(format!(
1569 "verilator-{evidence_label}-build-{}.log",
1570 target.name
1571 ));
1572 let build_log = format!(
1573 "command: verilator --binary --timing -Wno-fatal -Wno-DECLFILENAME -Wno-UNUSEDSIGNAL --threads 1 -j 1 --top-module {testbench_top} --Mdir {} {} {}\nelapsed_seconds: {:.3}\nstatus: {}\nstdout:\n{}\nstderr:\n{}\n",
1574 object_dir.display(),
1575 verilog.display(),
1576 testbench_path.display(),
1577 build_elapsed.as_secs_f64(),
1578 build.status,
1579 build_stdout.trim(),
1580 build_stderr.trim()
1581 );
1582 fs::write(&build_log_path, build_log).with_context(|| {
1583 format!(
1584 "writing {evidence_label} simulation build log {}",
1585 build_log_path.display()
1586 )
1587 })?;
1588 if !build.status.success() {
1589 bail!(
1590 "Verilator could not build the {evidence_label} testbench for {} after {:.3}s; diagnostic log: {}:\nstdout:\n{}\nstderr:\n{}",
1591 target.name,
1592 build_elapsed.as_secs_f64(),
1593 render_artifact_location(&build_log_path, keep),
1594 build_stdout.trim(),
1595 build_stderr.trim()
1596 );
1597 }
1598 Ok(VerilatorPhaseEvidence {
1599 elapsed: build_elapsed,
1600 log_path: build_log_path,
1601 })
1602}
1603
1604fn run_verilator_testbench(
1605 target: &TargetSpec,
1606 output_dir: &Path,
1607 keep: bool,
1608 testbench_top: &str,
1609 pass_marker: &str,
1610 object_dir: &Path,
1611 evidence_label: &str,
1612) -> Result<VerilatorPhaseEvidence> {
1613 let executable = object_dir.join(format!("V{testbench_top}"));
1614 let simulation_started = Instant::now();
1615 let simulation = ProcessCommand::new(&executable)
1616 .env("TMPDIR", output_dir)
1617 .env("TMP", output_dir)
1618 .env("TEMP", output_dir)
1619 .output()
1620 .with_context(|| {
1621 format!(
1622 "running Verilator {evidence_label} simulation {}",
1623 executable.display(),
1624 )
1625 })?;
1626 let simulation_elapsed = simulation_started.elapsed();
1627 let simulation_stdout = String::from_utf8_lossy(&simulation.stdout);
1628 let simulation_stderr = String::from_utf8_lossy(&simulation.stderr);
1629 let simulation_log_path = output_dir.join(format!(
1630 "verilator-{evidence_label}-simulation-{}.log",
1631 target.name,
1632 ));
1633 let simulation_log = format!(
1634 "elapsed_seconds: {:.3}\nstatus: {}\nstdout:\n{}\nstderr:\n{}\n",
1635 simulation_elapsed.as_secs_f64(),
1636 simulation.status,
1637 simulation_stdout.trim(),
1638 simulation_stderr.trim()
1639 );
1640 fs::write(&simulation_log_path, simulation_log).with_context(|| {
1641 format!(
1642 "writing {evidence_label} simulation log {}",
1643 simulation_log_path.display()
1644 )
1645 })?;
1646 if !simulation.status.success() || !simulation_stdout.contains(pass_marker) {
1647 bail!(
1648 "Verilator {evidence_label} simulation failed for {} after {:.3}s; expected marker `{}`; diagnostic log: {}:\nstdout:\n{}\nstderr:\n{}",
1649 target.name,
1650 simulation_elapsed.as_secs_f64(),
1651 pass_marker,
1652 render_artifact_location(&simulation_log_path, keep),
1653 simulation_stdout.trim(),
1654 simulation_stderr.trim()
1655 );
1656 }
1657 Ok(VerilatorPhaseEvidence {
1658 elapsed: simulation_elapsed,
1659 log_path: simulation_log_path,
1660 })
1661}
1662
1663fn render_artifact_location(path: &Path, keep: bool) -> String {
1664 if keep {
1665 format!("{} (preserved by --keep)", path.display())
1666 } else {
1667 format!(
1668 "{} (ephemeral; removed when the check exits, use --keep to preserve)",
1669 path.display()
1670 )
1671 }
1672}
1673
1674fn require_iverilog(temp_dir: &Path) -> Result<()> {
1675 let output = ProcessCommand::new("iverilog")
1676 .env("TMPDIR", temp_dir)
1677 .env("TMP", temp_dir)
1678 .env("TEMP", temp_dir)
1679 .arg("-V")
1680 .output()
1681 .map_err(|error| {
1682 if error.kind() == std::io::ErrorKind::NotFound {
1683 anyhow!(
1684 "Icarus Verilog (`iverilog`) is required for `rhdl-gen check` but was not found in PATH"
1685 )
1686 } else {
1687 anyhow!(error)
1688 }
1689 })?;
1690 if !output.status.success() {
1691 bail!(
1692 "Icarus Verilog is present but `iverilog -V` failed: {}",
1693 String::from_utf8_lossy(&output.stderr).trim()
1694 );
1695 }
1696 Ok(())
1697}
1698
1699fn elaborate_with_iverilog(target: &TargetSpec, verilog: &Path, output_dir: &Path) -> Result<()> {
1700 let elaborated = output_dir.join(format!("{}.ivl", target.name));
1701 let output = ProcessCommand::new("iverilog")
1702 .env("TMPDIR", output_dir)
1703 .env("TMP", output_dir)
1704 .env("TEMP", output_dir)
1705 .arg("-g2012")
1706 .arg("-s")
1707 .arg(target.top_module)
1708 .arg("-o")
1709 .arg(&elaborated)
1710 .arg(verilog)
1711 .output()
1712 .with_context(|| format!("invoking Icarus for target {}", target.name))?;
1713 if !output.status.success() {
1714 bail!(
1715 "Icarus syntax/elaboration failed for target {} (top {}):\nstdout:\n{}\nstderr:\n{}",
1716 target.name,
1717 target.top_module,
1718 String::from_utf8_lossy(&output.stdout).trim(),
1719 String::from_utf8_lossy(&output.stderr).trim()
1720 );
1721 }
1722 Ok(())
1723}
1724
1725static CHECK_DIRECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
1726
1727struct CheckWorkspace {
1728 path: PathBuf,
1729 keep: bool,
1730}
1731
1732impl CheckWorkspace {
1733 fn create(root: &Path, keep: bool) -> Result<Self> {
1734 for _ in 0..100 {
1735 let counter = CHECK_DIRECTORY_COUNTER.fetch_add(1, Ordering::Relaxed);
1736 let path = root.join(format!("rhdl-gen-check-{}-{counter}", std::process::id()));
1737 match fs::create_dir(&path) {
1738 Ok(()) => {
1739 let canonical = fs::canonicalize(&path).with_context(|| {
1740 format!(
1741 "canonicalizing temporary check directory {}",
1742 path.display()
1743 )
1744 });
1745 let path = match canonical {
1746 Ok(path) => path,
1747 Err(error) => {
1748 let _ = fs::remove_dir(&path);
1749 return Err(error);
1750 }
1751 };
1752 return Ok(Self { path, keep });
1753 }
1754 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
1755 Err(error) => {
1756 return Err(error).with_context(|| {
1757 format!("creating temporary check directory {}", path.display())
1758 });
1759 }
1760 }
1761 }
1762 bail!(
1763 "could not allocate a unique check directory beneath {}",
1764 root.display()
1765 )
1766 }
1767
1768 fn path(&self) -> &Path {
1769 &self.path
1770 }
1771}
1772
1773impl Drop for CheckWorkspace {
1774 fn drop(&mut self) {
1775 if !self.keep {
1776 let _ = fs::remove_dir_all(&self.path);
1777 }
1778 }
1779}
1780
1781#[cfg(test)]
1782mod tests {
1783 use super::*;
1784
1785 static TEST_DIRECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
1786
1787 struct TestDirectory(PathBuf);
1788
1789 impl TestDirectory {
1790 fn new(label: &str) -> Self {
1791 let counter = TEST_DIRECTORY_COUNTER.fetch_add(1, Ordering::Relaxed);
1792 let path = std::env::temp_dir().join(format!(
1793 "rhdl-gen-test-{}-{counter}-{label}",
1794 std::process::id()
1795 ));
1796 fs::create_dir(&path).expect("create selected test directory");
1797 Self(path)
1798 }
1799 }
1800
1801 impl Drop for TestDirectory {
1802 fn drop(&mut self) {
1803 let _ = fs::remove_dir_all(&self.0);
1804 }
1805 }
1806
1807 fn fixture_provenance() -> SourceProvenance {
1808 SourceProvenance {
1809 commit: "0123456789abcdef0123456789abcdef01234567".to_owned(),
1810 dirty: false,
1811 }
1812 }
1813
1814 #[allow(
1815 clippy::too_many_lines,
1816 reason = "the supported-command matrix is kept in one test so every parsed variant remains auditable together"
1817 )]
1818 #[test]
1819 fn parses_supported_commands() {
1820 assert_eq!(parse_args(["list"]).expect("list parses"), Command::List);
1821 assert_eq!(
1822 parse_args([
1823 "generate",
1824 "legacy-keccak-round",
1825 "--output",
1826 "/tmp/example",
1827 "--force",
1828 ])
1829 .expect("generate parses"),
1830 Command::Generate {
1831 target: "legacy-keccak-round".to_owned(),
1832 output: PathBuf::from("/tmp/example"),
1833 force: true,
1834 }
1835 );
1836 assert_eq!(
1837 parse_args(["check", "--full", "--temp-root", "/tmp", "--keep"]).expect("check parses"),
1838 Command::Check {
1839 temp_root: Some(PathBuf::from("/tmp")),
1840 keep: true,
1841 full: true,
1842 signer_full: false,
1843 }
1844 );
1845 assert_eq!(
1846 parse_args(["check", "--signer-full"]).expect("signer check parses"),
1847 Command::Check {
1848 temp_root: None,
1849 keep: false,
1850 full: false,
1851 signer_full: true,
1852 }
1853 );
1854 assert_eq!(
1855 parse_args([
1856 "signer-throughput",
1857 "--source-evidence",
1858 "/tmp/source",
1859 "--output",
1860 "/tmp/output",
1861 ])
1862 .expect("signer throughput parses"),
1863 Command::SignerThroughput {
1864 source_evidence: PathBuf::from("/tmp/source"),
1865 output: PathBuf::from("/tmp/output"),
1866 }
1867 );
1868 assert_eq!(
1869 parse_args(["benchmark-kernel", "--output", "/tmp/benchmark"])
1870 .expect("benchmark kernel parses"),
1871 Command::BenchmarkKernel {
1872 output: PathBuf::from("/tmp/benchmark"),
1873 }
1874 );
1875 assert_eq!(
1876 parse_args([
1877 "u280-link-graph",
1878 "--kernel",
1879 "/tmp/kernel.v",
1880 "--kernel-manifest",
1881 "/tmp/kernel.manifest.json",
1882 "--signer-evidence",
1883 "/tmp/signer-evidence",
1884 "--output",
1885 "/tmp/combined.json",
1886 ])
1887 .expect("U280 link graph parses"),
1888 Command::U280LinkGraph {
1889 kernel: PathBuf::from("/tmp/kernel.v"),
1890 kernel_manifest: PathBuf::from("/tmp/kernel.manifest.json"),
1891 signer_evidence: PathBuf::from("/tmp/signer-evidence"),
1892 output: PathBuf::from("/tmp/combined.json"),
1893 }
1894 );
1895 assert_eq!(
1896 parse_args([
1897 "u280-verify-link-graph",
1898 "--kernel",
1899 "/tmp/kernel.v",
1900 "--kernel-manifest",
1901 "/tmp/kernel.manifest.json",
1902 "--signer-evidence",
1903 "/tmp/signer-evidence",
1904 "--record",
1905 "/tmp/combined.json",
1906 ])
1907 .expect("U280 link graph verifier parses"),
1908 Command::U280VerifyLinkGraph {
1909 kernel: PathBuf::from("/tmp/kernel.v"),
1910 kernel_manifest: PathBuf::from("/tmp/kernel.manifest.json"),
1911 signer_evidence: PathBuf::from("/tmp/signer-evidence"),
1912 record: PathBuf::from("/tmp/combined.json"),
1913 }
1914 );
1915 assert_eq!(
1916 parse_args([
1917 "u280-hbm-rtl-check",
1918 "--kernel",
1919 "/tmp/kernel.v",
1920 "--kernel-manifest",
1921 "/tmp/kernel.manifest.json",
1922 "--signer-evidence",
1923 "/tmp/signer-evidence",
1924 "--record",
1925 "/tmp/combined.json",
1926 "--output",
1927 "/tmp/rtl-evidence",
1928 ])
1929 .expect("U280 HBM RTL check parses"),
1930 Command::U280HbmRtlCheck {
1931 kernel: PathBuf::from("/tmp/kernel.v"),
1932 kernel_manifest: PathBuf::from("/tmp/kernel.manifest.json"),
1933 signer_evidence: PathBuf::from("/tmp/signer-evidence"),
1934 record: PathBuf::from("/tmp/combined.json"),
1935 output: PathBuf::from("/tmp/rtl-evidence"),
1936 }
1937 );
1938 assert_eq!(
1939 parse_args([
1940 "__signer-lower",
1941 "lane",
1942 "--output",
1943 "/tmp/lane.v",
1944 "--source-commit",
1945 "0123456789abcdef",
1946 ])
1947 .expect("internal signer lowerer parses"),
1948 Command::InternalSignerLower {
1949 kind: "lane".to_owned(),
1950 output: PathBuf::from("/tmp/lane.v"),
1951 source_commit: "0123456789abcdef".to_owned(),
1952 }
1953 );
1954 }
1955
1956 #[allow(
1957 clippy::too_many_lines,
1958 reason = "the rejected-command matrix is kept in one test so incomplete and duplicate cases remain auditable together"
1959 )]
1960 #[test]
1961 fn rejects_incomplete_or_duplicate_arguments() {
1962 assert!(parse_args(["generate", "legacy-keccak-round"]).is_err());
1963 assert!(
1964 parse_args([
1965 "generate",
1966 "legacy-keccak-round",
1967 "-o",
1968 "one",
1969 "--output",
1970 "two",
1971 ])
1972 .is_err()
1973 );
1974 assert!(parse_args(["list", "extra"]).is_err());
1975 assert!(parse_args(["unknown"]).is_err());
1976 assert!(parse_args(["check", "--full", "--full"]).is_err());
1977 assert!(parse_args(["check", "--signer-full", "--signer-full"]).is_err());
1978 assert!(parse_args(["signer-throughput", "--output", "one"]).is_err());
1979 assert!(parse_args(["benchmark-kernel"]).is_err());
1980 assert!(parse_args(["benchmark-kernel", "--output", "one", "extra"]).is_err());
1981 assert!(
1982 parse_args([
1983 "u280-link-graph",
1984 "--kernel",
1985 "kernel.v",
1986 "--output",
1987 "combined.json",
1988 ])
1989 .is_err()
1990 );
1991 assert!(
1992 parse_args([
1993 "u280-link-graph",
1994 "--kernel",
1995 "one.v",
1996 "--kernel",
1997 "two.v",
1998 "--kernel-manifest",
1999 "kernel.json",
2000 "--signer-evidence",
2001 "signer-evidence",
2002 "--output",
2003 "combined.json",
2004 ])
2005 .is_err()
2006 );
2007 assert!(
2008 parse_args([
2009 "u280-verify-link-graph",
2010 "--kernel",
2011 "kernel.v",
2012 "--kernel-manifest",
2013 "kernel.json",
2014 "--signer-evidence",
2015 "signer-evidence",
2016 ])
2017 .is_err()
2018 );
2019 assert!(
2020 parse_args([
2021 "u280-hbm-rtl-check",
2022 "--kernel",
2023 "kernel.v",
2024 "--kernel-manifest",
2025 "kernel.json",
2026 "--signer-evidence",
2027 "signer-evidence",
2028 "--record",
2029 "combined.json",
2030 ])
2031 .is_err()
2032 );
2033 assert!(
2034 parse_args([
2035 "u280-hbm-rtl-check",
2036 "--kernel",
2037 "one.v",
2038 "--kernel",
2039 "two.v",
2040 "--kernel-manifest",
2041 "kernel.json",
2042 "--signer-evidence",
2043 "signer-evidence",
2044 "--record",
2045 "combined.json",
2046 "--output",
2047 "evidence",
2048 ])
2049 .is_err()
2050 );
2051 assert!(
2052 parse_args([
2053 "u280-verify-link-graph",
2054 "--kernel",
2055 "kernel.v",
2056 "--kernel-manifest",
2057 "one.json",
2058 "--kernel-manifest",
2059 "two.json",
2060 "--signer-evidence",
2061 "signer-evidence",
2062 "--record",
2063 "combined.json",
2064 ])
2065 .is_err()
2066 );
2067 assert!(
2068 parse_args([
2069 "signer-throughput",
2070 "--source-evidence",
2071 "one",
2072 "--source-evidence",
2073 "two",
2074 "--output",
2075 "three",
2076 ])
2077 .is_err()
2078 );
2079 assert!(parse_args(["__signer-lower", "farm"]).is_err());
2080 }
2081
2082 #[test]
2083 fn registry_is_profile_explicit_and_easy_to_extend() {
2084 let expected_names = [
2085 "legacy-keccak-round",
2086 "legacy-keccak-permutation",
2087 "legacy-keccak-iterative",
2088 "sha256-round",
2089 "sha256-compression-oracle",
2090 "sha256-pipeline-2",
2091 "sha256-pipeline-64-unchecked",
2092 "sha256-compression-lane-64-unchecked",
2093 "sha256-compression-farm-3x64-unchecked",
2094 "sha256-u280-hbm-kernel",
2095 ];
2096 assert_eq!(TARGETS.len(), 10);
2097 assert_eq!(
2098 TARGETS.iter().map(|target| target.name).collect::<Vec<_>>(),
2099 expected_names.to_vec()
2100 );
2101 for target in TARGETS {
2102 assert!(matches!(
2103 target.profile,
2104 "LEGACY_KECCAK" | "HASHSIGS_SHA256_GENERIC_V1"
2105 ));
2106 assert!(
2107 Path::new(target.verilog_filename)
2108 .extension()
2109 .is_some_and(|extension| extension.eq_ignore_ascii_case("v"))
2110 );
2111 assert!(target.manifest_filename.ends_with(".manifest.json"));
2112 assert!(!target.validation_scope.is_empty());
2113 }
2114 assert_eq!(
2115 find_target("sha256-pipeline-64-unchecked")
2116 .expect("unchecked target is registered")
2117 .profile,
2118 "HASHSIGS_SHA256_GENERIC_V1"
2119 );
2120 }
2121
2122 #[test]
2123 fn generation_is_deterministic_and_manifest_digest_matches() {
2124 let first = TestDirectory::new("deterministic-first");
2125 let second = TestDirectory::new("deterministic-second");
2126 let target = find_target("legacy-keccak-round").expect("registered target");
2127 let provenance = fixture_provenance();
2128 let first_artifact =
2129 generate_target(target, &first.0, false, &provenance).expect("first generation");
2130 let second_artifact =
2131 generate_target(target, &second.0, false, &provenance).expect("second generation");
2132
2133 let first_verilog = fs::read(&first_artifact.verilog_path).expect("read first Verilog");
2134 let second_verilog = fs::read(&second_artifact.verilog_path).expect("read second Verilog");
2135 assert_eq!(first_verilog, second_verilog);
2136 assert_eq!(
2137 fs::read(&first_artifact.manifest_path).expect("read first manifest"),
2138 fs::read(&second_artifact.manifest_path).expect("read second manifest")
2139 );
2140 assert_eq!(
2141 first_artifact.manifest.verilog_sha256,
2142 hex::encode(Sha256::digest(&first_verilog))
2143 );
2144 assert_eq!(
2145 first_artifact.manifest.evidence_tier,
2146 GENERATED_VERILOG_EVIDENCE_TIER
2147 );
2148 assert_eq!(
2149 first_artifact.manifest.validation_scope,
2150 target.validation_scope
2151 );
2152 assert_eq!(first_artifact.manifest.source_git_commit, provenance.commit);
2153 assert!(!first_artifact.manifest.source_git_dirty);
2154 }
2155
2156 #[test]
2157 fn overwrite_requires_force_and_preserves_existing_content() {
2158 let directory = TestDirectory::new("overwrite");
2159 let target = find_target("legacy-keccak-round").expect("registered target");
2160 let provenance = fixture_provenance();
2161 let artifact =
2162 generate_target(target, &directory.0, false, &provenance).expect("initial generation");
2163 let original = fs::read(&artifact.verilog_path).expect("read initial output");
2164 let error = generate_target(target, &directory.0, false, &provenance)
2165 .expect_err("overwrite must fail");
2166 assert!(error.to_string().contains("refusing to overwrite"));
2167 assert_eq!(
2168 fs::read(&artifact.verilog_path).expect("read preserved output"),
2169 original
2170 );
2171 generate_target(target, &directory.0, true, &provenance).expect("forced generation");
2172 assert_eq!(
2173 fs::read(&artifact.verilog_path).expect("read forced output"),
2174 original
2175 );
2176 }
2177
2178 #[test]
2179 fn throughput_output_requires_a_new_directory_outside_source_evidence() {
2180 let source = TestDirectory::new("throughput-source-evidence");
2181 let output_parent = TestDirectory::new("throughput-output-parent");
2182 assert!(
2183 create_throughput_output_directory(&source.0, &source.0).is_err(),
2184 "an existing directory must never be reused"
2185 );
2186 assert!(
2187 create_throughput_output_directory(&source.0.join("nested"), &source.0).is_err(),
2188 "the immutable source-evidence directory must never receive output"
2189 );
2190 let selected = output_parent.0.join("new-evidence");
2191 let created = create_throughput_output_directory(&selected, &source.0)
2192 .expect("create new external throughput output");
2193 assert!(created.is_dir());
2194 assert_eq!(
2195 created,
2196 fs::canonicalize(&selected).expect("canonical new output")
2197 );
2198 }
2199
2200 #[test]
2201 fn default_generation_targets_contain_their_declared_top_modules() {
2202 for target in TARGETS
2203 .iter()
2204 .filter(|target| target.default_unit_generation)
2205 {
2206 let generated = (target.generate)().expect("target lowers");
2207 assert!(
2208 generated.contains(&format!("module {}", target.top_module)),
2209 "target {} lacks top {}",
2210 target.name,
2211 target.top_module
2212 );
2213 }
2214 }
2215
2216 #[test]
2217 fn slow_and_unchecked_targets_are_not_in_default_generation_loops() {
2218 for name in [
2219 "sha256-compression-oracle",
2220 "sha256-pipeline-64-unchecked",
2221 "sha256-compression-lane-64-unchecked",
2222 "sha256-compression-farm-3x64-unchecked",
2223 ] {
2224 assert!(
2225 !find_target(name)
2226 .expect("special target is registered")
2227 .default_unit_generation
2228 );
2229 }
2230 assert!(
2231 find_target("sha256-pipeline-64-unchecked")
2232 .expect("unchecked target is registered")
2233 .validation_scope
2234 .contains("not RHDL/RTL equivalence evidence")
2235 );
2236 }
2237
2238 #[test]
2239 fn full_check_support_files_make_their_scope_and_lifetime_explicit() {
2240 assert!(full_sha_check::lane_testbench().contains(full_sha_check::LANE_PASS_MARKER));
2241 assert!(full_sha_check::farm_testbench().contains(full_sha_check::FARM_PASS_MARKER));
2242 assert!(
2243 render_artifact_location(Path::new("example.log"), false)
2244 .contains("ephemeral; removed when the check exits")
2245 );
2246 assert!(
2247 render_artifact_location(Path::new("example.log"), true)
2248 .contains("preserved by --keep")
2249 );
2250 }
2251
2252 #[test]
2253 fn verilator_warning_inventory_is_code_specific_and_deterministic() {
2254 let stderr = "%Warning-UNUSEDSIGNAL: first\n%Warning-UNOPTFLAT: second\ncontinuation\n%Warning-UNUSEDSIGNAL: third\n";
2255 let inventory = verilator_warning_inventory(stderr);
2256 assert_eq!(inventory.get("UNOPTFLAT"), Some(&1));
2257 assert_eq!(inventory.get("UNUSEDSIGNAL"), Some(&2));
2258 assert_eq!(
2259 render_warning_inventory(&inventory),
2260 "UNOPTFLAT=1, UNUSEDSIGNAL=2"
2261 );
2262 assert_eq!(render_warning_inventory(&BTreeMap::new()), "none");
2263 }
2264}