Files
retroSoC/scripts/crypto_p1.py
T
maksyuki 3c63fec8dc feat: refactor crypto test paths and add new tests for firmware validation
- Updated test paths in  to point to the new directory structure for crypto files.
- Introduced  to validate firmware identity and reject stale firmware based on source changes and simulation logs.
- Enhanced  to include additional memory status and control registers, ensuring alignment with RTL definitions.
2026-09-26 14:00:25 +08:00

531 lines
32 KiB
Python

#!/usr/bin/env python3
"""Focused CRYPTO-P1 evidence; all artifacts are separate from frozen P0."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import struct
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.crypto_constants import compact_header, pack, validate # noqa: E402
from scripts.crypto_p0 import ( # noqa: E402
COMMON, INTERFACES, RTL, UTILITIES, digest, dump, execute, inventory, oracle,
synthesis_metrics,
)
def input_hashes(sources: list[Path]) -> dict[str, str]:
# Compilation lists exclude include-only SVH files; their contents still
# affect the compiled design and must participate in stale-evidence checks.
paths = set(sources) | set((ROOT / "rtl/ip/security").glob("*.svh"))
paths |= {ROOT / "dependencies/dependencies.lock.json", ROOT / "rtl/mini/filelist/ip.fl"}
return {str(p.relative_to(ROOT)): digest(p) for p in sorted(paths)}
def firmware_inputs() -> dict[str, str]:
names = subprocess.check_output(["git", "ls-files", "--cached", "--others", "--exclude-standard",
"--", "rtl", "crt", "app", "scripts", "configs", "dependencies", "Makefile",
"tests/c/crypto_firmware.c"], cwd=ROOT, text=True).splitlines()
return {name: digest(ROOT / name) for name in sorted(set(names))
if (ROOT / name).is_file() and not name.endswith((".md", ".png", ".jpg"))
and not name.startswith("rtl/mini/formal/")}
def firmware_runtime_inputs(variant: Path) -> dict[str, str]:
"""Include generated CPU outputs and selected PDK models used by the run."""
variant = variant.resolve()
inputs = firmware_inputs()
generated = variant / "generated/vexiiriscv"
if generated.is_dir():
for path in sorted(generated.rglob("*")):
if path.is_file():
inputs[str(path.relative_to(ROOT))] = digest(path)
pdk = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/verilog"
for path in sorted(pdk.glob("RM_IHPSG13_1P_1024x32_c2_bm_bist.v")):
inputs[str(path.relative_to(ROOT))] = digest(path)
for path in sorted(pdk.glob("RM_IHPSG13_1P_core_behavioral_bm_bist.v")):
inputs[str(path.relative_to(ROOT))] = digest(path)
return inputs
def firmware_run(variant: Path, kind: str) -> None:
"""Capture the inputs before building/running, never at report assembly."""
output = variant / "crypto/p1/firmware-run.json"
manifest = json.loads((variant / "meta/manifest.json").read_text())
cfg = manifest["configuration"]
expected = {"PDK": "IHP130", "SIMU": "VERILATOR", "LINK_TYPE": "ld2_all_sram",
"HAVE_SVA": "YES", "APP": "ci_smoke" if kind == "ci" else "bringup"}
if kind == "ci":
expected["HAVE_CSR"] = "YES"
if any(cfg.get(k) != v for k, v in expected.items()):
raise ValueError(f"firmware variant configuration does not match {expected}")
timestamp = re.fullmatch(r"ihp130-(\d{4}-\d{2}-\d{2}-\d{2}-\d{2})-[0-9a-f]+", variant.name)
if timestamp is None:
raise ValueError("expected canonical IHP130 variant path")
command = ["make", "CONFIG=configs/ci/ihp130.mk", f"BUILD_TIMESTAMP={timestamp[1]}",
*[f"{k}={v}" for k, v in cfg.items()], "VERILATOR_SIM_ARGS=--fast-flash", "SOC_SIM_TIME=1800"]
if kind == "lp":
command += [f"APP_SRCS={ROOT / 'tests/c/crypto_firmware.c'}"]
command += ["firmware", "sim"]
tracked_inputs = firmware_inputs()
record = {"status": "running", "kind": kind, "manifest": manifest,
"source_sha256": tracked_inputs, "command": command}
dump(output, record)
try:
record["flow"] = execute(output.parent, "firmware-flow", command, timeout=3600)
current_tracked = firmware_inputs()
if current_tracked != tracked_inputs:
raise ValueError("tracked firmware inputs changed during the run")
runtime_inputs = firmware_runtime_inputs(variant)
result = variant / "sim/verilator/result-sim-check.json"
simulation = json.loads(result.read_text())
if simulation["status"] != "passed":
raise ValueError("firmware simulation did not pass")
paths = [result, Path(simulation["log"]), variant / "sw/firmware", variant / "meta/manifest.json"]
record.update(status="passed", source_sha256=runtime_inputs, simulation=simulation,
artifacts_sha256={str(p): digest(p) for p in paths})
except (OSError, ValueError, RuntimeError) as error:
record.update(status="failed", error=str(error))
raise
finally:
dump(output, record)
def checked_firmware(variant: Path, kind: str) -> dict:
path = variant / "crypto/p1/firmware-run.json"
record = json.loads(path.read_text())
if record.get("status") != "passed" or record.get("kind") != kind:
raise ValueError(f"missing successful {kind} run identity: {path}")
if record.get("source_sha256") != firmware_runtime_inputs(variant):
raise ValueError(f"stale firmware inputs: {path}")
for name, value in record["artifacts_sha256"].items():
if not Path(name).exists() or digest(Path(name)) != value:
raise ValueError(f"stale firmware artifact: {name}")
flow = json.loads(Path(record["flow"]["result"]).read_text())
if flow.get("status") != "passed" or flow.get("exit_code") != 0:
raise ValueError(f"firmware command did not succeed: {path}")
return {**record, "variant": str(variant), "run_record": str(path), "run_record_sha256": digest(path)}
def quality(output: Path) -> None:
paths = sorted({*RTL, *(ROOT / "rtl/mini/formal").glob("crypto_*.sv"),
*(ROOT / "tests/rtl").glob("crypto_*_tb.sv")} -
{ROOT / "tests/rtl/crypto_stream_tb.sv", ROOT / "tests/rtl/crypto_primitives_tb.sv",
ROOT / "tests/rtl/crypto_apb_tb.sv", ROOT / "tests/rtl/crypto_dma_tb.sv"})
results = []
for path in paths:
command = ["verible-verilog-format", "--flagfile=.verible-format", "--failsafe_success=false",
"--verify", str(path)]
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
results.append({"command": command, "exit_code": result.returncode,
"diagnostic": result.stdout + result.stderr})
passed = all(item["exit_code"] == 0 for item in results)
dump(output / "format.json", {"status": "passed" if passed else "failed", "checks": results,
"source_sha256": input_hashes(paths)})
if not passed:
raise ValueError(f"Crypto formatting failed: {output / 'format.json'}")
execute(output, "ruff", ["ruff", "check", "."])
execute(output, "quality-gates", ["make", "sw-format-check", "sw-policy-check", "sw-host-test",
"rtl-style-check", "rtl-readiness-check"])
dump(output / "quality-inputs.json", {"source_sha256": firmware_inputs()})
def inputs(output: Path) -> None:
output.mkdir(parents=True, exist_ok=True)
payload = pack(ROOT)
if payload != oracle().constant_image() or payload != pack(ROOT):
raise ValueError("CRYC1 independent oracle/reproducibility failure")
(output / "crypto.cryc").write_bytes(payload)
(output / "constants.hex").write_text(
"".join(f"{word:08x}\n" for (word,) in struct.iter_unpack("<I", payload)))
(output / "crypto_constants.h").write_text(compact_header(payload))
lengths = (0, 1, 3, 55, 56, 63, 64, 65, 127, 128, 129)
(output / "sha.hex").write_text("".join(
hashlib.new(name, bytes(range(length))).hexdigest().ljust(64, "0") + "\n"
for name in ("sha224", "sha256") for length in lengths))
fixture = oracle().rsa_fixture()
(output / "rsa.hex").write_text("".join(f"{fixture[key]:0512x}\n" for key in
("modulus", "private", "message", "ciphertext", "n0_prime", "montgomery")))
dump(output / "constants.json", {**validate(payload), "status": "passed",
"compact_bytes": 848, "oracle": "tests/crypto_reference.py"})
def rtl(output: Path, full_rsa: bool, macro: bool, jobs: int, top: str = "crypto_v2_tb",
simulator: str = "verilator", response_delay: int = 0,
duplicate_response: bool = False, drop_response_mask: int = 0) -> None:
inputs(output)
case = output / ("ihp-functional" if macro else "fallback")
if top != "crypto_v2_tb":
case = case / top
if simulator == "icarus":
case = case / "icarus"
case.mkdir(parents=True, exist_ok=True)
(case / "ccache-tmp").mkdir(exist_ok=True)
sources = [*INTERFACES, *UTILITIES, *[p for p in RTL if p.suffix == ".sv"]]
defines = ["-DSV_ASSRT_DISABLE"]
if response_delay:
defines += [f"-DCRYPTO_RESPONSE_DELAY={response_delay}"]
if duplicate_response:
defines += ["-DCRYPTO_RESPONSE_DUPLICATE"]
if drop_response_mask:
defines += [f"-DCRYPTO_RESPONSE_DROP_MASK={drop_response_mask}"]
includes = [ROOT / "rtl/ip/security", COMMON]
if top == "crypto_dma_v2_tb":
generated = case / "memory_map"
execute(case, "memory-map", [sys.executable, str(ROOT / "rtl/mini/address_map/generate_memory_map.py"),
"--map", str(ROOT / "rtl/mini/address_map/memory_map.json"),
"--output-dir", str(generated), "--have-sram-if", "NO"])
includes += [generated / "rtl", COMMON / "interface", COMMON / "stream", COMMON / "utils"]
sources = [COMMON / "interface/axi4_if.sv", *sources, COMMON / "stream/round_robin_arbiter.sv"]
sources += [ROOT / "rtl/ip/peripheral" / name for name in
("dma_pkg.sv", "dma_req_if.sv", "dma_axi4_master.sv", "dma_core.sv")]
if macro:
sources += [ROOT / "rtl/tech/tc_sram.sv"]
models = list((ROOT / "physical/pdk").rglob("RM_IHPSG13_1P_1024x32_c2_bm_bist.v"))
if len(models) != 1:
raise ValueError(f"expected exactly one IHP130 1024x32 model, found {models}")
sources += [*models, models[0].with_name("RM_IHPSG13_1P_core_behavioral_bm_bist.v")]
# SYNTHESIS omits the vendor's unsupported Verilator #0 A_DLY probe;
# the actual FUNCTIONAL array/bit-mask model remains fully enabled.
defines += ["-DPDK_IHP130", "-DHAVE_SRAM_MACRO", "-DFUNCTIONAL"]
if simulator == "verilator":
defines += ["-DSYNTHESIS"]
sources += [ROOT / "tests/rtl" / f"{top}.sv"]
command = ["verilator", "--binary", "--timing", "-Wno-fatal", "--timescale", "1ns/1ps", "--top-module", top,
"--Mdir", str(case / "obj"), "-j", str(jobs), *defines,
*[f"-I{path}" for path in includes], *map(str, sources)]
evidence = {"status": "running", "full_rsa": full_rsa, "macro": macro, "simulator": simulator,
"production_sha256": input_hashes(sources)}
dump(case / "functional.json", evidence)
try:
if simulator == "icarus":
fl = case / "sources.fl"
fl.write_text("\n".join([*[f"+define+{item[2:]}" for item in defines],
*[f"+incdir+{path}" for path in includes], *map(str, sources)]) + "\n")
converted = case / "test.v"
evidence["convert"] = execute(case, "convert", [sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"), "-f", str(fl), "--output", str(converted)])
evidence["compile"] = execute(case, "compile", ["iverilog", "-g2012", "-s", top,
"-o", str(case / "test.vvp"), str(converted)])
simulation = ["vvp", str(case / "test.vvp")]
else:
evidence["compile"] = execute(case, "compile", command, env={
"CCACHE_DIR": str(case / "ccache"), "CCACHE_TEMPDIR": str(case / "ccache-tmp")})
simulation = [str(case / "obj" / f"V{top}")]
simulation += [f"+constants={output / 'constants.hex'}",
f"+sha={output / 'sha.hex'}", f"+rsa={output / 'rsa.hex'}"]
if full_rsa:
simulation += ["+full_rsa"]
markers = {"crypto_dma_v2_tb": "CRYPTO_V2_DMA_PASS", "crypto_storage_tb": "CRYPTO_V2_STORAGE_PASS",
"crypto_concurrent_tb": "CRYPTO_V2_CONCURRENT_PASS",
"crypto_response_fault_tb": "CRYPTO_V2_RESPONSE_FAULT_PASS",
"crypto_v2_tb": "CRYPTO_V2_PASS"}
evidence["simulation"] = execute(case, "simulation", simulation, timeout=3600, marker=markers[top])
if top == "crypto_dma_v2_tb":
evidence["error_simulations"] = {name: execute(case, name, [*simulation, f"+{name}"],
marker="CRYPTO_V2_DMA_ERROR_PASS") for name in ("dma_read_error", "dma_write_error")}
evidence["status"] = "passed"
except (OSError, ValueError, RuntimeError) as error:
evidence.update(status="failed", error=str(error))
raise
finally:
dump(case / "functional.json", evidence)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=("constants", "rtl", "synth", "formal", "report", "firmware", "quality"))
parser.add_argument("--variant-root", type=Path, required=True)
parser.add_argument("--full-rsa", action="store_true")
parser.add_argument("--macro", action="store_true")
parser.add_argument("--jobs", type=int, default=4)
parser.add_argument("--case", choices=("crypto_v2_tb", "crypto_dma_v2_tb", "crypto_storage_tb", "crypto_concurrent_tb", "crypto_response_fault_tb"), default="crypto_v2_tb")
parser.add_argument("--simulator", choices=("verilator", "icarus"), default="verilator")
parser.add_argument("--response-delay", type=int, default=0)
parser.add_argument("--duplicate-response", action="store_true")
parser.add_argument("--drop-response-mask", type=lambda value: int(value, 0), default=0)
parser.add_argument("--baseline-root", type=Path)
parser.add_argument("--lp-variant-root", type=Path)
parser.add_argument("--ci-variant-root", type=Path)
parser.add_argument("--firmware-kind", choices=("lp", "ci"), default="lp")
args = parser.parse_args()
variant = args.variant_root.resolve()
manifest = json.loads((variant / "meta/manifest.json").read_text())
if manifest["profile"] != "ihp130":
raise ValueError("use configs/ci/ihp130.mk")
output = variant / "crypto/p1"
if args.command == "constants":
inputs(output)
elif args.command == "synth":
synth(variant, output)
elif args.command == "formal":
formal(output)
elif args.command == "firmware":
firmware_run(variant, args.firmware_kind)
elif args.command == "quality":
quality(output)
elif args.command == "report":
if args.baseline_root is None or args.lp_variant_root is None:
parser.error("report requires --baseline-root and --lp-variant-root")
report_all(variant, output, args.baseline_root.resolve(), args.lp_variant_root.resolve(),
args.ci_variant_root.resolve() if args.ci_variant_root else None)
else:
rtl(output, args.full_rsa, args.macro, args.jobs, args.case, args.simulator,
args.response_delay, args.duplicate_response, args.drop_response_mask)
def report_all(variant: Path, output: Path, baseline: Path, lp_variant: Path,
ci_variant: Path | None = None) -> None:
"""Summarize evidence without upgrading focused tests to release acceptance."""
manifest = json.loads((variant / "meta/manifest.json").read_text())
source_files = [*RTL, ROOT / "scripts/data/crypto_constants.json",
ROOT / "scripts/crypto_constants.py", ROOT / "scripts/crypto_p1.py",
ROOT / "crt/src/hal/crypto.c", ROOT / "crt/src/hal/crypto_lifecycle.c",
ROOT / "crt/include/retrosoc/hal/crypto.h", ROOT / "crt/include/retrosoc/hal/crypto_regs.h"]
identity = {"schema_version": 1, "feature": "crypto", "phase": "CRYPTO-P1",
"manifest": manifest, "report_command": sys.argv,
"source_snapshot_sha256": {str(p.relative_to(ROOT)): digest(p) for p in source_files}}
dump(output / "p1-report.json", {**identity, "status": "running"})
paths = ("ihp-functional", "ihp-functional/crypto_dma_v2_tb", "ihp-functional/crypto_storage_tb",
"ihp-functional/crypto_concurrent_tb", "ihp-functional/crypto_response_fault_tb",
"ihp-functional/icarus", "fallback", "fallback/crypto_storage_tb")
cases = {}
for name in paths:
path = output / name / "functional.json"
case = json.loads(path.read_text()) if path.exists() else {"status": "NOT_RUN"}
stale = [p for p, value in case.get("production_sha256", {}).items()
if not (ROOT / p).exists() or digest(ROOT / p) != value]
if stale:
case = {"status": "stale", "paths": stale}
cases[name] = {"path": str(path), "sha256": digest(path) if path.exists() else None, **case}
functional_pass = all(case.get("status") == "passed" for case in cases.values())
functional_pass &= cases["ihp-functional"].get("full_rsa") is True
dump(output / "functional.json", {**identity, "status": "passed" if functional_pass else "incomplete", "cases": cases})
cycle_values = {}
for name in ("ihp-functional", "ihp-functional/crypto_dma_v2_tb"):
log = output / name / "simulation.log"
if log.exists():
cycle_values.update({key: int(value) for key, value in re.findall(
r"CRYPTO_CYCLES operation=(\w+) cycles=(\d+)", log.read_text())})
dump(output / "cycles.json", {**identity, "status": "passed" if functional_pass else "incomplete",
"pclk_cycles": cycle_values, "counter_policy": "32-bit hardware counters; testbench bounds below wrap",
"evidence": [str(output / name / "simulation.log") for name in paths]})
proofs = json.loads((output / "formal.json").read_text())
formal_stale = [p for proof in proofs.get("proofs", {}).values()
for p, value in proof.get("source_sha256", {}).items() if digest(ROOT / p) != value]
proof_pass = proofs.get("status") == "passed" and len(proofs.get("proofs", {})) == 5 and not formal_stale
dump(output / "lifecycle.json", {**identity,
"status": "passed" if functional_pass and proof_pass else "incomplete",
"formal": str(output / "formal.json"), "stale_formal_sources": formal_stale,
"directed_coverage": ["boot scrub", "short/partial/CRC/padding rejection", "readback corruption/retry/lock",
"all-key-size AES directions and multi-block/tails", "22 SHA padding cases", "held DMA output abort",
"simultaneous AES/SHA/RSA", "FIFO and all mutable rows inspected after erase", "scrub fault/fatal latch",
"reset during load/verify", "private RSA verify success/failure with equal cycles"],
"coverage_limit": "Universal control interruption properties plus directed physical erasure/fault tests; see coverage.json for the exact decomposition."})
dma = cases["ihp-functional/crypto_dma_v2_tb"].get("error_simulations", {})
boundary_log = (output / "ihp-functional/simulation.log").read_text()
coverage = {
"command_completion": {"evidence": "crypto_v2_tb + crypto_apb_formal", "passed": functional_pass and proof_pass},
"padding_readback": {"evidence": "CRC-neutral bank-0 rows 528/529 corruption", "passed": functional_pass},
"timeout_lock_boundary": {"evidence": "six public-APB zeroize/COMMIT boundary cases",
"passed": "CRYPTO_V2_CANCEL_BOUNDARIES_PASS" in boundary_log},
"fatal_stream_drain": {"evidence": "crypto_stream_formal + SHA FIFO fault with held AES output",
"passed": proof_pass and "CRYPTO_V2_FATAL_DMA_DRAIN_PASS" in boundary_log},
"dma_axi_errors": {"evidence": dma, "passed": len(dma) == 2},
"response_fault_model": {"evidence": "crypto_response_fault_tb", "passed":
cases["ihp-functional/crypto_response_fault_tb"].get("status") == "passed"},
"state_interruption": {"evidence": "universal engine next-state abort/zeroize assertions, arbitrary lifecycle reset, bounded scrub proof, physical bank/FIFO inspections",
"passed": functional_pass and proof_pass},
}
coverage_pass = all(case["passed"] for case in coverage.values())
dump(output / "coverage.json", {**identity, "status": "passed" if coverage_pass else "incomplete",
"requirements": coverage, "method": "Proofs quantify over reachable states; physical data tests inspect all rows/slots. This does not claim arbitrary silicon fault or analog SRAM coverage."})
old = json.loads((baseline / "crypto/p0/baseline-synthesis.json").read_text())
new = json.loads((output / "candidate-synthesis.json").read_text())
old_inventory = json.loads((baseline / "crypto/p0/storage-inventory.json").read_text())["checkpoints"]["pre_memory_design"]
new_inventory = json.loads((output / "storage-inventory.json").read_text())["checkpoints"]["pre_memory_design"]
recipe_equal = old["recipe_sha256"] == new["recipe_sha256"]
for field in ("YOSYS_TARGET_PERIOD_PS", "YOSYS_FLATTEN_HIER", "SYNTH_RECIPE", "HAVE_SRAM_MACRO", "PDK"):
recipe_equal &= old["environment"][field] == new["environment"][field]
synthesis_stale = [p for p, value in new["production_sha256"].items() if digest(ROOT / p) != value]
synthesis_pass = new["status"] == "passed" and recipe_equal and not synthesis_stale
dump(output / "memory-synthesis-ab.json", {**identity,
"status": "passed" if synthesis_pass else "incomplete", "scope": "block observations",
"qualification": "P2 must repeat isolated clean-revision A/B; unrelated jobs overlapped these runs",
"same_recipe_and_target": recipe_equal, "stale_sources": synthesis_stale,
"baseline": {"report": str(baseline / "crypto/p0/baseline-synthesis.json"), "flow": old["flow"],
"inferred_bits": old_inventory["inferred_bits"], "register_bits": old_inventory["register_bits"]},
"candidate": {"report": str(output / "candidate-synthesis.json"), "flow": new["flow"],
"inferred_bits": new_inventory["inferred_bits"], "register_bits": new_inventory["register_bits"],
"macros": new_inventory["macros"]},
"duration_ratio": new["flow"]["duration_seconds"] / old["flow"]["duration_seconds"],
"rss_ratio": new["flow"]["peak_rss_kib"] / old["flow"]["peak_rss_kib"]})
try:
firmware = checked_firmware(lp_variant, "lp")
except (OSError, ValueError) as error:
firmware = {"status": "stale", "error": str(error), "variant": str(lp_variant)}
if ci_variant:
try:
ci = checked_firmware(ci_variant, "ci")
except (OSError, ValueError) as error:
ci = {"status": "stale", "error": str(error), "variant": str(ci_variant)}
else:
ci = {"status": "NOT_RUN"}
dump(output / "ci-smoke.json", {"schema_version": 1, "feature": "crypto", "phase": "CRYPTO-P1", **ci})
quality_path = output / "quality-gates.json"
quality = json.loads(quality_path.read_text()) if quality_path.exists() else {"status": "NOT_RUN"}
quality_identity = output / "quality-inputs.json"
if not quality_identity.exists() or json.loads(quality_identity.read_text())["source_sha256"] != firmware_inputs():
quality = {"status": "stale", "reason": "quality inputs missing or changed"}
warning_path = lp_variant / "meta/rtl-lint-warnings.json"
warnings = json.loads(warning_path.read_text()) if warning_path.exists() else {"status": "NOT_RUN"}
dump(output / "quality.json", {**identity, "gates": quality, "rtl_lint_warnings": warnings,
"format": json.loads((output / "format.json").read_text()) if (output / "format.json").exists() else {"status": "NOT_RUN"},
"pytest": "NOT_RUN (maintainer instruction)", "misra_deviations": []})
dump(output / "firmware.json", {"schema_version": 1, "feature": "crypto", "phase": "CRYPTO-P1", **firmware,
"scope": "Full SoC, focused LP public HAL initialization/AES/SHA/DMA/timeout/zeroize image"})
dump(output / "physical.json", {**identity, "status": "NOT_RUN", "phase": "CRYPTO-P2",
"gaps": ["whole-chip synthesis", "functional gate simulation", "STA", "macro placement and physical views",
"remaining PDK qualification", "isolated clean-revision synthesis A/B"]})
focused_pass = (functional_pass and proof_pass and coverage_pass and synthesis_pass and
firmware["status"] == "passed" and ci["status"] == "passed" and quality["status"] == "passed")
dump(output / "p1-report.json", {**identity, "status": "incomplete",
"focused_gates_status": "passed" if focused_pass else "incomplete",
"gates": {"functional": functional_pass, "formal": proof_pass, "block_synthesis": synthesis_pass,
"lp_firmware": firmware["status"] == "passed", "focused_quality": quality["status"] == "passed",
"ci_smoke": ci["status"] == "passed"},
"acceptance_gaps": ([] if ci["status"] == "passed" else [
"CI firmware provenance is stale or missing; rerun the locked profile with generated-input hashes"
if ci["status"] == "stale" else "Correctly configured ci_smoke has not passed"]) + [
"Independent review of the P1 fixes remains required",
"Whole-repository formatting and warning baseline results are separate from focused Crypto gates",
"Pytest not run by maintainer instruction; P2 qualification not started"],
"physical": "NOT_RUN (P2)",
"p0_report_matches_archive": (digest(output / "archive/p0-report.json") ==
digest(baseline / "crypto/p0/p0-report.json")) if (output / "archive/p0-report.json").exists() else None})
if not focused_pass:
raise ValueError("focused P1 evidence is missing/stale/failed; see p1-report.json")
def formal(output: Path) -> None:
solver = shutil.which("bitwuzla")
if solver is None:
raise FileNotFoundError("bitwuzla is required")
binary_dir = output / "formal/bin"
binary_dir.mkdir(parents=True, exist_ok=True)
wrapper = binary_dir / "bitwuzla"
shutil.copyfile(ROOT / "scripts/bitwuzla_smt2.py", wrapper)
wrapper.chmod(0o755)
report = {"status": "running", "proofs": {}}
dump(output / "formal.json", report)
try:
for top in ("crypto_control_formal", "crypto_rsa_release_formal", "crypto_scrub_progress_formal", "crypto_apb_formal", "crypto_stream_formal"):
case = output / "formal" / top
case.mkdir(parents=True, exist_ok=True)
names = ["crypto_mem_pkg.sv", "crypto_scrubber.sv"]
if top == "crypto_control_formal":
names += ["crypto_mem_ctrl.sv"]
elif top == "crypto_rsa_release_formal":
names += ["crypto_montgomery_sram.sv", "crypto_rsa_sram_core.sv"]
sources = [COMMON / "utils/register.sv", *[ROOT / "rtl/ip/security" / name for name in names],
ROOT / "rtl/mini/formal" / f"{top}.sv"]
if top == "crypto_apb_formal":
sources = [*INTERFACES, *UTILITIES, *[p for p in RTL if p.suffix == ".sv"],
ROOT / "rtl/mini/formal" / f"{top}.sv"]
elif top == "crypto_stream_formal":
sources = [*UTILITIES, *[ROOT / "rtl/ip/security" / name for name in
("crypto_pkg.sv", "crypto_mem_pkg.sv", "crypto_scrubber.sv", "crypto_clearable_fifo.sv",
"crypto_aes_sram_core.sv", "crypto_aes_sram_engine.sv")],
ROOT / "rtl/mini/formal" / f"{top}.sv"]
config = case / "prove.sby"
config.write_text("[options]\nmode prove\ndepth 24\n\n[engines]\n"
"smtbmc --presat --nounroll bitwuzla\n\n[script]\n"
f"read_slang -DSV_ASSRT_DISABLE -DFORMAL -USYNTHESIS -I{COMMON} --top {top} " + " ".join(map(str, sources)) +
("\nselect -assert-count 10 t:$check" if top == "crypto_rsa_release_formal" else "") +
f"\nprep -top {top}\nasync2sync\ndffunmap\nopt_clean\n")
hashes = input_hashes(sources)
result = execute(case, "proof", ["sby", "-f", "-d", str(case / "work"), str(config)],
timeout=600, env={"PATH": str(binary_dir) + os.pathsep + os.environ["PATH"],
"RETROSOC_BITWUZLA": solver})
if (case / "work/status").read_text().split()[0] != "PASS":
raise ValueError(f"{top} did not prove")
if hashes != input_hashes(sources):
raise ValueError(f"{top} sources changed during proof")
report["proofs"][top] = {**result, "status": "passed",
"source_sha256": hashes}
report["status"] = "passed"
except (OSError, ValueError, RuntimeError) as error:
report.update(status="failed", error=str(error))
raise
finally:
dump(output / "formal.json", report)
def synth(variant: Path, output: Path) -> None:
manifest = json.loads((variant / "meta/manifest.json").read_text())
cfg = manifest["configuration"]
if any(cfg[key] != value for key, value in {
"HAVE_SRAM_MACRO": "YES", "PDK_BEHAV": "NO", "SYNTH": "YOSYS",
"SYNTH_RECIPE": "balanced", "PDK": "IHP130",
}.items()):
raise ValueError("P1 block synthesis requires the unchanged IHP130 macro/balanced profile")
output.mkdir(parents=True, exist_ok=True)
work = Path(tempfile.mkdtemp(prefix="synth-", dir=output))
for name in ("out", "tmp", "rpt"):
(work / name).mkdir()
sources = [*INTERFACES, *UTILITIES, ROOT / "rtl/tech/tc_sram.sv",
*[p for p in RTL if p.suffix == ".sv"]]
fl = work / "crypto.fl"
fl.write_text("+define+PDK_IHP130\n+define+HAVE_SRAM_MACRO\n+define+SYNTHESIS\n+define+SV_ASSRT_DISABLE\n" +
f"+incdir+{ROOT / 'rtl/ip/security'}\n+incdir+{COMMON}\n" +
f"+incdir+{COMMON / 'interface'}\n" + "\n".join(map(str, sources)) + "\n")
directory = ROOT / "physical/smoke/syn/yosys/script"
period = subprocess.check_output([sys.executable, str(ROOT / "scripts/yosys_period.py"),
"--domains", str(ROOT / "rtl/mini/integration/clock_reset_domains.json"),
"--domain", "pclk"], text=True).strip()
env = {"PDK": "IHP130", "SOC": "MINI", "SYNTH_RECIPE": "balanced", "HAVE_SRAM_MACRO": "YES",
"SRAM_SIZE_KIB": "32", "SV_FLIST": str(fl), "TOP_DESIGN": "apb4_crypto",
"PROJ_NAME": "apb4_crypto", "WORK": str(work / "tmp"), "BUILD": str(work / "out"),
"REPORTS": str(work / "rpt"), "NETLIST": str(work / "out/crypto.v"),
"CONFIG": str(work / "out/crypto.config"), "YOSYS_TARGET_PERIOD_PS": period,
"YOSYS_FLATTEN_HIER": "1", "YOSYS_KEEP_HIER_INST": '"t:tc_sram*$*"',
"YOSYS_REPORT_INSTS": ""}
report = {"status": "running", "scope": "crypto_block", "manifest": manifest,
"environment": env, "production_sha256": input_hashes(sources),
"recipe_sha256": {p.name: digest(p) for p in
(directory / "synth.tcl", directory / "abc_balanced.script", directory / "crypto_block_synth.tcl")}}
dump(output / "candidate-synthesis.json", report)
try:
report["flow"] = execute(work, "yosys", ["yosys", "-t", "--perffile", str(work / "yosys-perf.json"),
"-c", str(directory / "crypto_block_synth.tcl")], timeout=10800, env=env)
report["metrics"] = synthesis_metrics(work / "rpt")
checkpoints = {p.stem: inventory(p) for p in (work / "rpt").glob("*_design.json")}
pre_memory = checkpoints["pre_memory_design"]
if (sum(pre_memory["macros"].values()) != 6 or pre_memory["rom_bits"] != 0 or
pre_memory["ram_bits"] > 1184):
raise ValueError("Crypto macro/inferred-storage contract failed")
report["status"] = "passed"
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
report.update(status="failed", error=str(error))
raise
finally:
dump(output / "candidate-synthesis.json", report)
dump(output / "storage-inventory.json", {"status": report["status"],
"production_sha256": report["production_sha256"],
"checkpoints": {p.stem: inventory(p) for p in (work / "rpt").glob("*_design.json")},
"whole_soc": "NOT_RUN (P2)", "physical": "NOT_RUN (P2)"})
if __name__ == "__main__":
main()