open-standards/wai/claim-conformance/_blake3.py
Transaction Science e8dbf68c78 wai: http_verify.py, the BLAKE3 tree in _blake3.py, and OH with chaining values
The corpus's second implementation, written from the extension text and SPEC
§7.1, standard library only: head, covered_len, the head proof (and each
committed proof rebuilt from the text), response checks, plans, selection,
the byte-range procedure with its fallback, codecs union, floor and support.
It agrees with wai_http_vectors on every case. It checks itself first against
a subset of the published BLAKE3 test vectors, its tree functions included;
`--mutate` (left-half, no-root, counter-zero, floor-as-codecs, blake3-iv)
makes it fail the cases each defect reaches; `diff` verifies the proofs
`wai_http_vectors diff` writes and refuses exactly the ones it flips.

claim-conformance/_blake3.py, already shared by claim_verify and
staged_verify, gains subtree_cv and parent_cv; its hash is unchanged
(claim_verify and staged_verify pass).

OH now carries a payload past its covered head: verified whole, the
counter-zero mutation had nothing to reach.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 07:51:43 -04:00

113 lines
4.2 KiB
Python

"""BLAKE3 in hash mode, written from its specification in pure Python.
Shared by the claim-conformance verifier (`claim_verify.py`), the staged-delivery
verifier (`../staged-conformance/staged_verify.py`) and the HTTP delivery verifier
(`../http-conformance/http_verify.py`), which import from here. It shares no code with
wai-rs, which computes the same hashes with the `blake3` crate.
`subtree_cv` and `parent_cv` expose the tree (the BLAKE3 specification, §2.1): the
non-root chaining value of a subtree that starts at a given byte offset of the whole
input, and a parent node with or without the ROOT flag. The HTTP head proof is built of
these (http-delivery §6).
"""
_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]
_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8
_M32 = 0xFFFFFFFF
def _rotr(x, n):
return ((x >> n) | (x << (32 - n))) & _M32
def _g(s, a, b, c, d, x, y):
s[a] = (s[a] + s[b] + x) & _M32
s[d] = _rotr(s[d] ^ s[a], 16)
s[c] = (s[c] + s[d]) & _M32
s[b] = _rotr(s[b] ^ s[c], 12)
s[a] = (s[a] + s[b] + y) & _M32
s[d] = _rotr(s[d] ^ s[a], 8)
s[c] = (s[c] + s[d]) & _M32
s[b] = _rotr(s[b] ^ s[c], 7)
def _compress(cv, m, counter, blen, flags):
s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, blen, flags]
m = list(m)
for r in range(7):
_g(s, 0, 4, 8, 12, m[0], m[1]); _g(s, 1, 5, 9, 13, m[2], m[3])
_g(s, 2, 6, 10, 14, m[4], m[5]); _g(s, 3, 7, 11, 15, m[6], m[7])
_g(s, 0, 5, 10, 15, m[8], m[9]); _g(s, 1, 6, 11, 12, m[10], m[11])
_g(s, 2, 7, 8, 13, m[12], m[13]); _g(s, 3, 4, 9, 14, m[14], m[15])
if r < 6:
m = [m[i] for i in _PERM]
return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)]
def _words(block):
block = block + b"\0" * (64 - len(block))
return [int.from_bytes(block[i:i + 4], "little") for i in range(0, 64, 4)]
def _chunk_output(chunk, counter):
blocks = [chunk[i:i + 64] for i in range(0, len(chunk), 64)] or [b""]
cv = _IV
for j, b in enumerate(blocks[:-1]):
cv = _compress(cv, _words(b), counter, 64, _CHUNK_START if j == 0 else 0)[:8]
last = blocks[-1]
return (cv, _words(last), counter, len(last), _CHUNK_END | (_CHUNK_START if len(blocks) == 1 else 0))
def _cv(out):
cv, w, c, bl, f = out
return _compress(cv, w, c, bl, f)[:8]
def blake3(data: bytes) -> bytes:
chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""]
stack = []
for idx, ch in enumerate(chunks[:-1]):
cv = _cv(_chunk_output(ch, idx))
total = idx + 1
while total & 1 == 0:
cv = _cv((_IV, stack.pop() + cv, 0, 64, _PARENT))
total >>= 1
stack.append(cv)
out = _chunk_output(chunks[-1], len(chunks) - 1)
while stack:
out = (_IV, stack.pop() + _cv(out), 0, 64, _PARENT)
cv, w, _, bl, f = out
return b"".join(x.to_bytes(4, "little") for x in _compress(cv, w, 0, bl, f | _ROOT)[:8])
def _bytes(words):
return b"".join(x.to_bytes(4, "little") for x in words)
def _left_chunks(n):
"""The left subtree of `n` chunks (n >= 2): the largest power of two below n."""
return 1 << ((n - 1).bit_length() - 1)
def subtree_cv(data: bytes, start: int) -> bytes:
"""The non-root chaining value of `data` as a whole subtree of the input whose first
chunk is at byte offset `start` (a multiple of 1024): chunk counters run from
start // 1024."""
assert start % 1024 == 0
chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""]
def node(cs, c0):
if len(cs) == 1:
return _cv(_chunk_output(cs[0], c0))
k = _left_chunks(len(cs))
return _cv((_IV, node(cs[:k], c0) + node(cs[k:], c0 + k), 0, 64, _PARENT))
return _bytes(node(chunks, start // 1024))
def parent_cv(left: bytes, right: bytes, root: bool = False) -> bytes:
"""A parent node over two chaining values: its chaining value, or with `root` the
hash it outputs as the root."""
m = [int.from_bytes((left + right)[i:i + 4], "little") for i in range(0, 64, 4)]
return _bytes(_compress(_IV, m, 0, 64, _PARENT | (_ROOT if root else 0))[:8])