#!/usr/bin/env python3 """Regenerate the φ_PRG anchor-map known-answer-test fixture. Writes ``bench/fixtures/phi-prg/known-answer-tests.jsonl`` from the fixed (seed, hard_hash, dim_h) list below. Each entry records the SHA-256 of the raw HMAC-SHA-512 expansion output (before float conversion) — the durable contract; the float layout can change without invalidating the fixture (the byte stream doesn't). Run this whenever the φ_PRG algorithm changes — and then bump ``PHI_PRG_VERSION`` in ``arborist/substrate/anchor_prg.py`` so the fixture's ``version`` field changes too. The KAT regression test (``tests/test_anchor_prg.py::test_phi_prg_known_answer_tests``) pins these values and asserts the version matches the module. Usage:: python -m scripts.generate_phi_prg_kat # from repo root # or: python scripts/generate_phi_prg_kat.py Config list (10 entries): the placeholder seed against zero / all- ones hashes at several dim_h; a random (seed, hash) pair with two one-bit-flip variants (avalanche cross-check); block-boundary cases (dim_h=16 = exactly one HMAC block; dim_h=17 = two blocks with truncation); a 4096-element counter-rollover stress sample. Note: at counter=0 the bytes are identical regardless of endianness, so the single-block (dim_h ≤ 16) entries are endianness-invariant; the multi-block entries pin the little-endian counter encoding. """ from __future__ import annotations import hashlib import json import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from arborist.substrate.anchor_prg import _expand, PHI_PRG_VERSION # noqa: E402 _FIXTURE = ( pathlib.Path(__file__).resolve().parents[1] / "bench" / "fixtures" / "phi-prg" / "known-answer-tests.jsonl" ) # (label, seed_hex, hard_hash_hex, dim_h) _ENTRIES: list[tuple[str, str, str, int]] = [ ("placeholder-seed/zero-hash/dim_h=1", "ef532720a49159beb6816d98e13a162bac63c531b631bd1adb0fcca96b467ff3", "0000000000000000000000000000000000000000000000000000000000000000", 1), ("placeholder-seed/zero-hash/dim_h=8", "ef532720a49159beb6816d98e13a162bac63c531b631bd1adb0fcca96b467ff3", "0000000000000000000000000000000000000000000000000000000000000000", 8), ("placeholder-seed/zero-hash/dim_h=32", "ef532720a49159beb6816d98e13a162bac63c531b631bd1adb0fcca96b467ff3", "0000000000000000000000000000000000000000000000000000000000000000", 32), ("placeholder-seed/all-ones-hash/dim_h=16", "ef532720a49159beb6816d98e13a162bac63c531b631bd1adb0fcca96b467ff3", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16), ("seed=A/hash=B/dim_h=64", "14ab2dab0d3cddeaa58ec70632d3ff4f5de2c514004a92144e260dfe384d912e", "a23cb10b94660f062b467f313a9dc9d84f2dc2748c3627c661082dda7f55e3bb", 64), ("seed=A/hash=B'/dim_h=64 (one-bit-flip from prior)", "14ab2dab0d3cddeaa58ec70632d3ff4f5de2c514004a92144e260dfe384d912e", "223cb10b94660f062b467f313a9dc9d84f2dc2748c3627c661082dda7f55e3bb", 64), ("seed=A'/hash=B/dim_h=64 (one-bit-flip seed)", "94ab2dab0d3cddeaa58ec70632d3ff4f5de2c514004a92144e260dfe384d912e", "a23cb10b94660f062b467f313a9dc9d84f2dc2748c3627c661082dda7f55e3bb", 64), ("block-boundary/dim_h=16", "cfd60c2bda64ebcefbb23a5b28d98269c9c4f8b8ac77f6f9ca7a0f4865b10f58", "724cd966a7bfe78ba802877510ffb90c67f385a1d3135e4e1b8a1b38f744c6da", 16), ("block-boundary/dim_h=17", "cfd60c2bda64ebcefbb23a5b28d98269c9c4f8b8ac77f6f9ca7a0f4865b10f58", "724cd966a7bfe78ba802877510ffb90c67f385a1d3135e4e1b8a1b38f744c6da", 17), ("stress/dim_h=4096", "0ddd62c311f88ebe2d4f6cd5d9d1374474dfd645e012043648dd966a71785c95", "e605ede3d9d0d13c6d7d32c5c424b998677eef0689a0d9f0fa4ebd1bb4307cb9", 4096), ] def build_lines() -> list[str]: lines = [ f"# arborist v7 phi_prg known-answer tests — version {PHI_PRG_VERSION}", "# Regenerated by scripts/generate_phi_prg_kat.py. Records SHA-256 of", "# the raw HMAC-SHA-512 (little-endian counter-mode) expansion output,", "# before float conversion. Algorithm change MUST bump PHI_PRG_VERSION", "# and re-run this script; old runs replay against the old fixture.", ] for label, seed_hex, hh_hex, dim_h in _ENTRIES: seed = bytes.fromhex(seed_hex) hh = bytes.fromhex(hh_hex) raw = _expand(seed, hh, dim_h * 4) lines.append(json.dumps({ "label": label, "version": PHI_PRG_VERSION, "seed_hex": seed_hex, "hard_hash_hex": hh_hex, "dim_h": dim_h, "output_sha256": hashlib.sha256(raw).hexdigest(), "output_bytes": dim_h * 4, }, ensure_ascii=False)) return lines def main() -> int: lines = build_lines() _FIXTURE.parent.mkdir(parents=True, exist_ok=True) _FIXTURE.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"wrote {len(_ENTRIES)} KAT entries to {_FIXTURE} (version {PHI_PRG_VERSION})") return 0 if __name__ == "__main__": raise SystemExit(main())