ticket #000035: flip φ_PRG counter big-endian → little-endian to match v7 TLV
v7's canonical integer byte-order was confirmed little-endian by
inspecting merkle-agi-dag_v7.txt §A1 — every to_bytes / astype in the
TLV encoding is little-endian (TLV length prefixes to_bytes(4,'little'),
enc_int to_bytes(8,'little'), quantized tensors '<i8'); no big-endian
anywhere. Per dav1d's 2026-05-11 review rule ("if v7 TLV canonical
integer encoding is little-endian, flip §3.4 to little-endian before
KAT freeze"), flip done — this is the -le variant.
Implementation (arborist/substrate/anchor_prg.py):
- PHI_PRG_VERSION → "phi-prg-v1-hmac-sha512-le" (still "v1";
the -le suffix records the endianness; future re-flip MUST bump).
- _expand: counter.to_bytes(4, 'big') → 'little'.
- _bytes_to_floats: int.from_bytes(..., 'big') → 'little' (the
uint32-word interpretation, for full consistency with v7).
- Module + function docstrings updated: little-endian throughout,
with the merkle-agi-dag_v7.txt §A1 verification note.
- Note: at counter=0 the bytes are identical regardless of
endianness, so 5 of the 10 KAT entries (dim_h ≤ 16, single block)
keep the same output_sha256; the 5 multi-block entries (dim_h 17/
32/64×3/4096) change.
KAT fixture (bench/fixtures/phi-prg/known-answer-tests.jsonl):
- Regenerated under the little-endian counter. Each entry now also
carries a "version" field (phi-prg-v1-hmac-sha512-le). Header
comment updated.
Tests (tests/test_anchor_prg.py, 30 → 31):
- test_module_exports_version_string: assert the -le suffix.
- test_bytes_to_floats_midpoint_maps_to_zero: 2^31 is b'\x00\x00\x00\x80'
in little-endian, not b'\x80\x00\x00\x00'.
- New test_bytes_to_floats_reads_little_endian: pins the byte-order
so an accidental re-flip is caught.
- test_phi_prg_first_block_matches_direct_hmac: uint32-word reads
little-endian (counter=0 bytes unchanged either way).
- test_phi_prg_known_answer_tests: assert kat['version'] == module
version when present.
Spec text (#000035 §3.4): folded the little-endian variant of
dav1d's §9.10 wording — counter_le32, uint32_le word reads, an
"all integers little-endian, matching v7 TLV §A1" preamble, and an
"Endianness — RESOLVED 2026-05-11" note replacing the open
big-vs-little question. soft-hash-channel-analysis.md §9.2/§11 +
#000035 status + TICKETS.md row updated. AUTOCOUNT for
test_anchor_prg.py bumped 30 → 31; PHI_PRG_VERSION refs in docs
bumped to -le.
Full suite: 2312 passed, 28 skipped.
This commit is contained in:
parent
d78dccc8ed
commit
101b101281
9 changed files with 144 additions and 94 deletions
|
|
@ -77,8 +77,21 @@ def test_bytes_to_floats_max_uint32_just_below_one():
|
|||
|
||||
|
||||
def test_bytes_to_floats_midpoint_maps_to_zero():
|
||||
# 0x80000000 → 2 * 0.5 - 1 = 0.0.
|
||||
assert _bytes_to_floats(b"\x80\x00\x00\x00") == [0.0]
|
||||
# u32 = 2^31 → 2 * 0.5 - 1 = 0.0. Words are read little-endian
|
||||
# (matches v7's TLV convention), so the byte representation of
|
||||
# 2^31 is b"\x00\x00\x00\x80", not b"\x80\x00\x00\x00".
|
||||
assert int.from_bytes(b"\x00\x00\x00\x80", "little") == 1 << 31
|
||||
assert _bytes_to_floats(b"\x00\x00\x00\x80") == [0.0]
|
||||
|
||||
|
||||
def test_bytes_to_floats_reads_little_endian():
|
||||
"""Sanity-pin the endianness: 0x00000001 in little-endian byte
|
||||
layout is b"\x01\x00\x00\x00" → u32=1 → 2*(1/2^32)-1 ≈ -1.0.
|
||||
The big-endian reading (u32 = 0x01000000 = 2^24) would give a
|
||||
very different float — this test catches an accidental flip back."""
|
||||
out = _bytes_to_floats(b"\x01\x00\x00\x00")
|
||||
assert out[0] == pytest.approx(2.0 * (1 / 2**32) - 1.0, abs=1e-12)
|
||||
assert out[0] < -0.999999
|
||||
|
||||
|
||||
# ----------------------------------------------------------- chi² sanity
|
||||
|
|
@ -215,33 +228,37 @@ def test_phi_prg_output_is_prefix_extending():
|
|||
|
||||
def test_phi_prg_first_block_matches_direct_hmac():
|
||||
"""First HMAC-SHA-512 block of phi_prg's output should match
|
||||
a direct hmac.new(seed, hard_hash + b'\\x00\\x00\\x00\\x00',
|
||||
sha512).digest() invocation. Hand-computed against the
|
||||
function's spec (#000035 §3.1 + module §1).
|
||||
a direct hmac.new(seed, hard_hash + counter_le_4, sha512).digest()
|
||||
invocation. Hand-computed against the function's spec (#000035
|
||||
§3.1/§3.4 + module §1). Counter and uint32-word reads are both
|
||||
little-endian (matches v7's TLV convention; dav1d review
|
||||
2026-05-11) — at counter=0 the bytes are b'\\x00\\x00\\x00\\x00'
|
||||
regardless of endianness, but the float-word interpretation is
|
||||
the part this test pins as little-endian.
|
||||
|
||||
Pattern from fox's `test_b1_exact_formula`: don't rely on
|
||||
KAT regression alone — compute the first-principles math in
|
||||
the test file and assert exact agreement. Catches algorithm
|
||||
drift that KAT regenerated against a buggy version would
|
||||
miss.
|
||||
drift that KAT regenerated against a buggy version would miss.
|
||||
"""
|
||||
import hmac
|
||||
|
||||
seed = hashlib.sha256(b"hand-formula-seed").digest()
|
||||
h = hashlib.sha256(b"hand-formula-hash").digest()
|
||||
|
||||
# Spec: out = HMAC-SHA-512(seed, hard_hash || counter_be_4) for
|
||||
# Spec: out = HMAC-SHA-512(seed, hard_hash || counter_le_4) for
|
||||
# counter = 0, 1, 2, ...; concatenated; truncated to dim_h * 4 bytes.
|
||||
expected_block_0 = hmac.new(
|
||||
seed, h + (0).to_bytes(4, "big"), hashlib.sha512
|
||||
seed, h + (0).to_bytes(4, "little"), hashlib.sha512
|
||||
).digest()
|
||||
# First block is 64 bytes = 16 uint32s = 16 floats. dim_h=16
|
||||
# consumes exactly the first block.
|
||||
floats = phi_prg(h, dim_h=16, seed=seed)
|
||||
# Convert expected_block_0 to floats per §2.3 spec.
|
||||
# Convert expected_block_0 to floats per §3.4 spec: uint32 read
|
||||
# little-endian, then 2*(u32/2^32)-1.
|
||||
expected_floats = []
|
||||
for i in range(16):
|
||||
u32 = int.from_bytes(expected_block_0[4 * i : 4 * (i + 1)], "big")
|
||||
u32 = int.from_bytes(expected_block_0[4 * i : 4 * (i + 1)], "little")
|
||||
expected_floats.append(2.0 * (u32 / 2 ** 32) - 1.0)
|
||||
for j, (got, exp) in enumerate(zip(floats, expected_floats)):
|
||||
assert got == pytest.approx(exp, abs=1e-12), (
|
||||
|
|
@ -332,12 +349,16 @@ def test_phi_prg_rejects_dim_h_above_counter_ceiling():
|
|||
|
||||
|
||||
def test_module_exports_version_string():
|
||||
assert PHI_PRG_VERSION == "phi-prg-v1-hmac-sha512"
|
||||
# The "-le" suffix records the 2026-05-11 big-endian → little-endian
|
||||
# counter flip (dav1d review; matches v7's TLV convention). A future
|
||||
# endianness or formula change MUST bump this token + regen the KATs.
|
||||
assert PHI_PRG_VERSION == "phi-prg-v1-hmac-sha512-le"
|
||||
# versioned-default discipline (calculator-test-patterns.md §2):
|
||||
# "v1" substring present so future major-version rotations
|
||||
# (v2-blake3-expansion etc.) are detectable at the call site
|
||||
# without string-comparing module paths.
|
||||
assert "v1" in PHI_PRG_VERSION
|
||||
assert PHI_PRG_VERSION.endswith("-le")
|
||||
|
||||
|
||||
def test_placeholder_seed_is_32_bytes():
|
||||
|
|
@ -391,6 +412,14 @@ def test_phi_prg_known_answer_tests():
|
|||
f"KAT mismatch for label={kat.get('label')!r}: "
|
||||
f"expected {kat['output_sha256']}, got {digest}"
|
||||
)
|
||||
# Version pin (present in 2026-05-11+ fixtures): a KAT recorded
|
||||
# under a different PHI_PRG_VERSION must not be silently replayed
|
||||
# against the current algorithm.
|
||||
if "version" in kat:
|
||||
assert kat["version"] == PHI_PRG_VERSION, (
|
||||
f"KAT version mismatch for label={kat.get('label')!r}: "
|
||||
f"fixture {kat['version']}, module {PHI_PRG_VERSION}"
|
||||
)
|
||||
# Also assert the float list has the expected length so a
|
||||
# bug in _bytes_to_floats truncation is caught.
|
||||
assert len(out_floats) == dim_h
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue