arborist/docs/tickets/ticket-000035-prg-choice-phi-prg.md
russell@unturf.com 8599ce3b2c
ticket #000036: add KAT-regen tooling + close
"One more iteration then close" (fox): added committed KAT-regeneration
scripts for both the T3 calculator and φ_PRG — the regen step was a
throwaway temp script before; now it's reproducible and the phi_prg
test's skipif reason ("run scripts/generate_phi_prg_kat.py") points at
a file that exists. Then closed #000036.

New scripts:
- scripts/generate_t3_bound_kat.py — regenerates
  bench/fixtures/t3-bound/known-answer-tests.jsonl from a fixed 12-config
  list (the §7 worked examples under max_envelope + non-default-C_B*
  + g=0 edge + explicit-b1_model pins for the other three models).
- scripts/generate_phi_prg_kat.py — regenerates
  bench/fixtures/phi-prg/known-answer-tests.jsonl from a fixed 10-entry
  list (placeholder/random seeds, one-bit-flip variants, block-boundary
  dim_h=16/17, 4096 counter-rollover stress).
- Both verified to reproduce the committed fixture data lines byte-
  for-byte (only the header comments changed, to reference the script).
  Each docstring states: run after any algorithm change, then bump the
  module version (CALCULATOR_VERSION / PHI_PRG_VERSION) so the fixture's
  version field changes too.

Doc/test:
- test_t3_bound_calculator.py skipif reason now references the regen
  script (matches the phi_prg test pattern).
- #000035 §3.3 + t3-bound.md §10.1 reference the regen scripts.

Closure (#000036):
- Status → closed · 2026-05-11 in the ticket file + TICKETS.md row.
  Phase 1 + dav1d Tier-1/Tier-2 (Option B in v1) + KAT-regen tooling
  all landed; all §5 acceptance criteria met; both dav1d closure
  blockers cleared. Continuation: empirical C_B1/C_B2/C_B3 tightening
  under #000043 (parks on v7 deployment data); landing the bound's
  framing into a v7 plastic-training spec parks on that spec gaining
  a deployment target; R2's architectural integrations (Merkle audit-
  event commitment, SQD canonicalization, CTI clause-lattice, 5F
  trigger, ForkScore security-risk) are separate tickets if wanted.
- t3-bound.md header flipped to "closed 2026-05-11".

Full suite: 2312 passed, 28 skipped.
2026-05-11 08:02:25 -04:00

16 KiB
Raw Blame History

Ticket #000035 — PRG choice for φ_PRG

Status: in progress · Phase 1 landed 2026-05-10; §9.10 spec amendment text dav1d-reviewed final 2026-05-11 (§3.4 below holds the folded version). Review decisions applied: manifest field renamed phi_prg_seed → anchor_prg_seed (purpose-scoped; phi_prg_seed kept as a code-local alias); float-map prose corrected ("uniform over a 2^32-point grid in [-1, 1) with negligible mean 2^32", not "unbiased"); dim_h ≤ 16·2^32 exhaustion guard added to arborist/substrate/anchor_prg.py (+ bool-dim_h reject); seed-independence / single-purpose-seed rule added; M1-enablement-policy separation (§9.10.1: skipping M1 governed by mitigation-selection policy, e.g. NO_ALIGNMENT per #000034, not by §9.10). Endianness RESOLVED 2026-05-11: v7's canonical TLV integer encoding is little-endian (merkle-agi-dag_v7.txt §A1), so per dav1d's review rule the φ_PRG counter + uint32-word reads were flipped big-endian → little-endian; PHI_PRG_VERSION → phi-prg-v1-hmac-sha512-le; the 10 KAT vectors regenerated; test_anchor_prg.py updated (30 → 31). HMAC-SHA-512 / 32-byte seed / SHALL-replace all LOCKED. Remaining: land §9.10 into the v7 plastic-training spec when that spec gains a deployment target (exogenous). Opened: 2026-05-09 Scope: Pin a specific cryptographic PRG construction for the M1 mitigation (PRG-based anchor map) proposed in #000018 §5.2 + §9.2. The §4 reduction in docs/soft-hash-channel-analysis.md treats φ_PRG as a random oracle; in deployment we need a deterministic PRG with explicit cryptographic strength assumption. Audience: maintainers of the v7 plastic-training spec; #000018 follow-up. Hard constraint: the PRG security parameter MUST match SHA-256 in the substrate (≥128-bit security level). Adding a weaker PRG would create a cryptographic-strength asymmetry — adversary breaks the PRG, channel re-opens.


1. Problem statement

#000018 §5.2 proposes φ_PRG as the M1 mitigation:

φ_PRG(digest) = PRG(seed = published, output_len = dim_h * 4 bytes)
                then bytes → floats in [-1, 1]

The §4 reduction proves channel-bound under the random-oracle model for φ_PRG. In deployment, "random oracle" becomes "PRG with public seed and computational indistinguishability from random."

The reduction holds modulo PRG security. Pick a weak PRG and the reduction's looseness is the PRG's distinguishing advantage. This ticket pins which PRG.

2. Design choices

2.1 Candidate PRG constructions

A. HMAC-SHA-512 expansion.

def phi_prg(digest_32bytes, dim_h):
    out = b""
    counter = 0
    while len(out) < dim_h * 4:
        out += hmac.new(seed, digest_32bytes + counter.to_bytes(4, 'big'),
                        hashlib.sha512).digest()
        counter += 1
    return bytes_to_floats(out[:dim_h * 4])
  • Strength: HMAC-SHA-512 is a PRF under the standard SHA-512 Merkle-Damgård + HMAC assumption. Distinguishing advantage is bounded by the SHA-512 collision-resistance bound (~2^256).
  • Dependency: keeps the substrate's hash family at SHA-{256,512} — already in the dependency surface.
  • Speed: ~1 GB/s on a modern x86 CPU; negligible per-checkpoint.

B. AES-256-CTR with public IV.

def phi_prg(digest_32bytes, dim_h):
    aes = AES.new(seed[:32], AES.MODE_CTR, nonce=digest_32bytes[:8])
    out = aes.encrypt(b"\\0" * (dim_h * 4))
    return bytes_to_floats(out)
  • Strength: AES-256 PRP under the standard AES key-recovery hardness assumption. Distinguishing advantage bounded by AES key search (~2^256) under known plaintext.
  • Dependency: adds cryptography package's AES (already in arborist core deps as cryptography>=42).
  • Speed: ~1.5 GB/s on x86 with AES-NI; faster than HMAC-SHA-512 but the difference doesn't matter at v7 checkpoint cadence.

C. ChaCha20 with public nonce.

  • Strength: ChaCha20 PRF under the cryptographic standard.
  • Dependency: cryptography provides; same as AES.
  • Considered: redundant with AES — pick one.

D. SHA-256 as a hash chain.

def phi_prg(digest_32bytes, dim_h):
    out = b""
    h = digest_32bytes
    while len(out) < dim_h * 4:
        h = hashlib.sha256(seed + h).digest()
        out += h
    return bytes_to_floats(out[:dim_h * 4])
  • Strength: Insufficient. SHA-256 hash chains are NOT guaranteed PRFs — known length-extension and predictable fixed-points make this unsuitable. Reject.

2.2 Recommendation

A — HMAC-SHA-512 expansion.

Reasons:

  1. Tightest dependency: stays inside the SHA family already committed to by SHA-256 (the substrate's hard-hash). Adding AES would multiply the assumption surface without cryptographic gain.
  2. Standard NIST-approved PRF construction (SP 800-108 KDF in counter mode using HMAC).
  3. Speed parity with AES at v7 cadence (per-checkpoint cost negligible compared to gradient computation).
  4. Provable security reduction: HMAC-SHA-512 PRF security reduces to SHA-512 collision-resistance, which is structurally stronger than SHA-256 (output space is 2× larger).

2.3 Float conversion

PRG outputs uniform-random bytes. The anchor space is [-1, 1]^dim_h. Convert via:

def bytes_to_floats(b):
    """uint32 → float in [-1, 1) via 2 * (u32/2^32) - 1."""
    n = len(b) // 4
    floats = []
    for i in range(n):
        u32 = int.from_bytes(b[4*i:4*(i+1)], 'big')
        floats.append(2.0 * (u32 / 2**32) - 1.0)
    return floats

Distribution: uniform on [-1, 1) modulo a 2^-32 quantization discontinuity; ignored at our precision.

2.4 Seed handling

The PRG seed is published (committed in v7 boot manifest). It's NOT a secret — secrecy isn't the security property. The property is computational indistinguishability of the OUTPUT from random, which holds even when the seed is public, because the adversary can't invert the PRF without solving the underlying SHA-512 problem.

Rotating the seed per-checkpoint becomes the M2 mitigation (#000018 §5.3) — orthogonal concern; M1 + M2 stack cleanly.

3. Implementation sketch

3.1 New module

arborist/v7/anchor_prg.py (new directory if v7 work hasn't landed yet; or wherever v7's plastic-training surface is implemented):

"""φ_PRG — HMAC-SHA-512 anchor map for v7 § 9.10."""
import hashlib
import hmac

PUBLISHED_SEED = b"..."  # 32-byte committed seed

def phi_prg(hard_hash_32: bytes, dim_h: int) -> list[float]:
    if len(hard_hash_32) != 32:
        raise ValueError("hard hash must be 32 bytes (SHA-256)")
    block_bytes = dim_h * 4
    out = bytearray()
    counter = 0
    while len(out) < block_bytes:
        msg = hard_hash_32 + counter.to_bytes(4, 'big')
        out += hmac.new(PUBLISHED_SEED, msg, hashlib.sha512).digest()
        counter += 1
    return _bytes_to_floats(bytes(out[:block_bytes]))

3.2 Tests

tests/test_anchor_prg.py:

  • Determinism: same input + same seed → same output bytes.
  • Distinguishing-from-random sanity: chi² test on a sample of outputs (loose; just catches gross bugs).
  • Boundary: dim_h=1, dim_h=10⁶ both produce sensible outputs.
  • Seed-change sensitivity: flipping one bit of the seed produces outputs uncorrelated with the original (Hamming distance ≈ output_size / 2).

3.3 Bench fixture

bench/fixtures/phi-prg/known-answer-tests.jsonl — fixed (seed, hard_hash, dim_h) → expected_output_sha256 triples (10 entries) for regression coverage. Regenerated by scripts/generate_phi_prg_kat.py (run it after any algorithm change, then bump PHI_PRG_VERSION so the fixture's version field changes too). tests/test_anchor_prg.py::test_phi_prg_known_answer_tests pins these values and asserts the version matches the module.

3.4 Spec amendment

v7 § 9.10 amendment text — dav1d-reviewed final (2026-05-11), little-endian variant. Decision set (RESPONSE 2026-05-11) + endianness resolution (2026-05-11): HMAC-SHA-512 — LOCK; manifest field anchor_prg_seed (purpose-scoped, not implementation-scoped — phi_prg_seed is an acceptable code-local alias only); seed size 32 bytes — LOCK; counter uint32 little-endian from 0, and the uint32-word interpretation is also little-endian — matching v7's canonical TLV integer encoding (verified against merkle-agi-dag_v7.txt §A1: TLV length prefixes to_bytes(4, "little"), enc_int is to_bytes(8, "little"), quantized tensors are <i8). Per dav1d's review rule ("if v7 TLV canonical integer encoding is little-endian, flip §3.4 to little-endian before KAT freeze"). Float map 2·(u32_le/2^32)1; prose says "uniform over a 2^32-point grid in [-1, 1) with negligible finite-grid mean 2^32", not "unbiased"; SHALL replace embed_hard_to_vec when M1 is enabled (whether M1 itself is mandatory is a separate mitigation-selection-policy question). Plus an explicit dim_h ≤ 16·2^32 exhaustion guard and a seed-independence requirement. PHI_PRG_VERSION carries the -le suffix (phi-prg-v1-hmac-sha512-le); the 10 KAT vectors were regenerated 2026-05-11 under the little-endian counter.

§ 9.10  Anchor PRG map φ_PRG

When M1 is enabled, the v7 anchor map φ_PRG(C(M), dim_h) is defined
as follows. All integers below are encoded little-endian, matching
v7's canonical TLV integer convention (§A1).

Let anchor_prg_seed be the v7 boot manifest's dedicated 32-byte PRG
seed. The seed MUST be generated independently of the model
checkpoint and training data, and MUST NOT be adversary-selected.
The seed MAY be public once committed, but it MUST be committed
before the corresponding anchor map is evaluated. anchor_prg_seed
is single-purpose — it MUST NOT be reused for any other PRG domain.

Let C(M) be the fixed-length committed model digest. Let counter
range over unsigned 4-byte little-endian integers starting at 0.

For counter = 0, 1, 2, … compute:

    block_counter = HMAC-SHA-512(anchor_prg_seed, C(M) ‖ counter_le32)

Concatenate successive blocks until at least dim_h · 4 bytes are
available; truncate to exactly dim_h · 4 bytes. A 4-byte counter
admits 2^32 HMAC-SHA-512 blocks of 64 bytes each, so dim_h MUST
satisfy dim_h ≤ 16 · 2^32; a request beyond that is an error.

Partition the byte stream into dim_h successive 4-byte words.
Interpret each word as an unsigned little-endian integer u32. Map
each u32 to a float by:

    x = 2 · (u32 / 2^32)  1

The resulting dim_h-vector is φ_PRG(C(M), dim_h).

This maps uniformly onto a 2^32-point grid in [-1, 1). The value
-1.0 is reachable; +1.0 is not. The finite-grid mean is 2^32,
which is negligible for the anchor-map use case. If exact
zero-mean sampling is ever required, the formula MUST be
version-bumped (e.g. to the midpoint map x = 2·((u32+0.5)/2^32)1)
rather than silently changed, and new KAT vectors emitted.

When M1 is enabled, φ_PRG SHALL replace the v7 reference linear
projection embed_hard_to_vec for the hard-anchor-to-vector map.
An operator MUST NOT claim M1 while continuing to use
embed_hard_to_vec.

§ 9.10.1  M1 enablement policy (non-normative pointer)

Whether a given deployment MUST enable M1 is decided by the
mitigation-selection policy, not by this section. M1 enablement
MAY be skipped only under an explicit policy rule — for example
when a current #000034 φ/Hessian alignment probe returns
NO_ALIGNMENT and the deployment policy accepts the residual risk.
Once M1 is enabled, § 9.10 above applies in full.

Endianness — RESOLVED 2026-05-11. v7's canonical integer byte-order was confirmed little-endian (merkle-agi-dag_v7.txt §A1 — every to_bytes/astype in the TLV encoding is little- endian; no big-endian anywhere). The §9.10 text above uses little-endian throughout to match. The reference implementation (arborist/substrate/anchor_prg.py), the 10 regenerated KAT vectors, and test_anchor_prg.py are all pinned to little-endian; PHI_PRG_VERSION = "phi-prg-v1-hmac-sha512-le". (The earlier big-endian draft + its KATs are superseded — a re-flip would need another PHI_PRG_VERSION bump.)

4. Out of scope

  • v7 spec full revision; this ticket lands the §9.10 amendment text, not the surrounding chapter.
  • M2 (per-checkpoint nonce) implementation; orthogonal.
  • Empirical Hessian-alignment measurement under φ_PRG (covered by #000034).
  • Performance tuning (PRG eval cost is ~µs per checkpoint; irrelevant).

5. Acceptance criteria

  1. arborist/v7/anchor_prg.py (or equivalent location) lands.
  2. Tests pin determinism + seed-sensitivity + boundary cases.
  3. Known-answer-test fixture committed under bench/fixtures.
  4. v7 § 9.10 amendment text drafted (this ticket §3.4 is the draft; final landing waits on v7 spec maintainer review).
  5. #000018 § 9.2 closes — the PRG choice question becomes "HMAC-SHA-512 with 32-byte committed seed."

6. Risks / Considerations

  • Future SHA-512 weakness. If SHA-512 is broken in a way that affects HMAC-SHA-512 PRF security, the M1 mitigation's cryptographic foundation cracks. Mitigation: the v7 manifest's phi_prg_version field lets future deployments swap to a successor PRF without breaking historical replay.
  • Seed compromise. If the published seed is somehow swapped by an adversary (e.g., supply-chain attack on the manifest), the PRG is still PRF-secure but the seed is now adversarial- controlled. The hard-hash chain (SHA-256 of the manifest) catches the swap as a chain-break, so this reduces to a manifest-integrity attack — out of scope for #000018, in scope for chain-check.

7. Status

In progress · Phase 1 landed 2026-05-10. Reference implementation shipped ahead of v7 plastic-training deployment target so the cryptographic primitive is unit-tested + KAT-pinned the moment v7 needs it.

Phase 1 — reference implementation (landed 2026-05-10)

  • arborist/substrate/anchor_prg.pyphi_prg(hard_hash_32, dim_h, *, seed) per §3.1; HMAC-SHA-512 counter-mode KDF; pure stdlib (hashlib + hmac); no third-party dependency. Module also exports PHI_PRG_VERSION = "phi-prg-v1-hmac-sha512-le" so future algorithm rotations can be detected at the call site without string-comparing module paths.

    Path note (2026-05-10): §3.1's original sketch placed the module at arborist/v7/anchor_prg.py. The version-prefixed namespace pattern was retired the same day in favour of arborist/substrate/ — the v in v7 referred to the substrate-paper version, which collided with the v9.8 SQLite schema version and confused readers about whether the dir tracked paper version or schema version. arborist/substrate/ is now the topic dir for paper-spec'd Merkle-AGI primitives, decoupled from any version number. Acceptance-criteria §5 item 1 reads through to "or equivalent location" so the move doesn't invalidate the original criterion.

  • tests/test_anchor_prg.py — 31 tests covering determinism, range invariants, chi² loose-uniformity sanity, dim_h boundary (1, 16384), seed-bit-flip avalanche, hash-bit-flip avalanche, hand-computed HMAC-SHA-512 first-block formula, prefix-extension closure, parametrized invalid-input cones, input validation (short/long/non-bytes hashes; non-positive / non-int dim_h), module export shape, and KAT regression. (Was 20; +7 from de997f7 2026-05-10 pattern backfill per docs/calculator-test-patterns.md.)

  • bench/fixtures/phi-prg/known-answer-tests.jsonl — 10 pinned KAT vectors covering the placeholder seed (smoke), block-boundary cases (dim_h=16 = exactly one HMAC-SHA-512 block; dim_h=17 = two blocks with truncation), seed/hash one-bit-flip variants, and a 4096-element stress sample to catch counter-rollover bugs. Bytes-output SHA-256 is the durable contract; float layout changes do NOT invalidate the fixture.

Phase 2 — v7 §9.10 amendment landing (not yet open)

§3.4 of this ticket holds the draft spec amendment text. Phase 2 lands it into the v7 spec proper when:

  1. The v7 plastic-training spec gains an active deployment target, AND
  2. The spec maintainer (currently fox) reviews the §3.4 text and confirms the §9.10 placement.

Until both signals fire, the amendment text stays here as a draft under the v7 spec maintainer's eyes.

Closure criterion

Closes when Phase 2 lands the §9.10 amendment into the v7 spec and #000018 §9.2 (which pins the open question "which PRG?") is resolved as "HMAC-SHA-512 with 32-byte committed seed."