From 3b9b81375c278517c673787355aecb671adc8e94 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 29 Jul 2026 14:59:26 -0300 Subject: [PATCH 01/14] spike(blake3): recover the oracle and gate-proved chip design from transcripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-07-23 BLAKE3 work — an independent reference implementation, a three-anchor oracle harness, a chip design and a z3 soundness gate — was written to a session scratchpad under /private/tmp, never committed, and the scratchpad was gone by the time anyone went looking. Nothing named blake exists anywhere in this repo's history. Recovered by replaying the Write and Edit tool calls out of the subagent transcripts (5 Writes + 11 Edits, every Edit applied cleanly). Committing it so this cannot happen a third time: the keccak spike's artifacts went the same way, and the EC campaign's audits spent two days in the same state before being pushed. Evidence the recovery is faithful rather than plausible-looking: the gate runs and its structural negative controls all fire, 5/5 — rot_wrong_amount / swap_g_operand / wrong_iv / drop_ff_xor / wrong_msg_index each SAT as designed. Two fixtures are absent because they were downloaded or generated rather than written by a tool call, so the transcript never held them: the upstream official_test_vectors.json, and canonical_6round_vectors.json (regenerated by running test_oracle.py). The gate's positive controls and oracle anchors 1-2 therefore have NOT been re-run. Anchor 3 needs nothing external — Plonky3 is vendored at others/Plonky3/blake3-air. README.md says exactly how to restore full validation. Status is unchanged from when it stopped: design gate-proved, no Rust written, blocked on a protocol decision — sign-off on a named "6-round collision resistance" assumption. The gate proves the chip matches the reference, not that 6 rounds are secure. One correction this recovery makes possible: the EC lincomb2 design study justified its NUMS assumption with "like blake3's 6-round assumption". No such assumption was ever recorded in the spec, because this work never shipped — it was cited as precedent for something that had not happened. --- thoughts/blake3/README.md | 91 +++ thoughts/blake3/blake3-chip/DESIGN.md | 411 +++++++++++++ .../blake3/blake3-chip/z3_blake_verify.py | 561 ++++++++++++++++++ thoughts/blake3/blake3-oracle/ORACLE.md | 368 ++++++++++++ thoughts/blake3/blake3-oracle/blake3_ref.py | 399 +++++++++++++ thoughts/blake3/blake3-oracle/test_oracle.py | 327 ++++++++++ 6 files changed, 2157 insertions(+) create mode 100644 thoughts/blake3/README.md create mode 100644 thoughts/blake3/blake3-chip/DESIGN.md create mode 100644 thoughts/blake3/blake3-chip/z3_blake_verify.py create mode 100644 thoughts/blake3/blake3-oracle/ORACLE.md create mode 100644 thoughts/blake3/blake3-oracle/blake3_ref.py create mode 100644 thoughts/blake3/blake3-oracle/test_oracle.py diff --git a/thoughts/blake3/README.md b/thoughts/blake3/README.md new file mode 100644 index 000000000..78a17680d --- /dev/null +++ b/thoughts/blake3/README.md @@ -0,0 +1,91 @@ +# BLAKE3 accelerator — oracle + gate-proved chip design (RECOVERED) + +**Provenance: recovered 2026-07-29 from subagent transcripts, not from a +backup.** The original work (2026-07-23) was written to a session scratchpad +under `/private/tmp/...`, never committed, and the scratchpad was gone by the +time anyone looked. The files here were reconstructed by replaying the `Write` +and `Edit` tool calls out of +`.claude/projects/.../1c23da47-.../subagents/agent-ablake3-{oracle,chip-design}-*.jsonl` +(5 Writes + 11 Edits, every Edit applied cleanly — no partial replays). + +Committing them is the point: this is the second EC/hash campaign whose +verification artifacts were nearly lost to a scratchpad. Anything worth keeping +belongs in the repo. + +## What this is + +A BLAKE3 compression-function accelerator that was taken to a **gate-proved +design** and stopped there, blocked on a protocol decision — sign-off on a named +"6-round collision resistance" assumption. No Rust was ever written. + +Purpose is **internal** (Merkle/Fiat–Shamir replacement candidate; the 6-round +variant is the primary target, with K12 as precedent). The EVM has no BLAKE3 — +only the BLAKE2b-F precompile at 0x09 (EIP-152), which is variable-round and +rarely used, so it stays guest code. + +## Contents + +| file | what it is | +|---|---| +| `blake3-oracle/blake3_ref.py` | independent reference implementation of the compression function | +| `blake3-oracle/test_oracle.py` | the three-anchor validation harness; also emits `canonical_6round_vectors.json` | +| `blake3-oracle/ORACLE.md` | anchor results and the contract map | +| `blake3-chip/DESIGN.md` | the chip design + §7 risk ledger | +| `blake3-chip/z3_blake_verify.py` | the soundness gate | + +## Status as recorded at the time + +Oracle **validated** against three independent anchors: the official +`test_vectors.json` (35×3), a differential against the `blake3` PyPI package +(92 cases), and Plonky3's `blake3-air` (20k compressions). + +Gate **PASS**: G-function UNSAT under free inputs (composition covers all +rounds, 6r and 7r), init/feed-forward UNSAT, 5 negative controls SAT, positive +controls pinned to oracle vectors. The field-level audit separated BV-observable +from field-only bug classes — dropping SLL `AreBytes` goes SAT, while dropping +carry booleanity goes SAT **only** in Int-mod-p, invisible to bitvectors. Same +phenomenon the keccak HWSL work hit. + +Layout: **1 row per compression, unrolled, ≈5,030 cell-equivalents for 6 +rounds ≈ 1/15 of keccak-f.** That figure corrected the oracle phase's own ¼–⅓ +estimate — blake would otherwise re-commit the 16-word message every row. +Contract map has **zero missing operations**: 32-bit XOR = 4× ByteAlu, +rotr16/8 are free byte relabels, rotr12/7 are HWSL shifts 4/9 (or inlined as +linear identities per the keccak rewrite), add = `emit_add_pair`. + +Design decision O1 resolved as option (c): two summed carry bits +`µ·(a+b+m−s−2^32(c1+c2))` at degree 2 plus booleanity at degree 3 — a ternary +carry is degree 4 and therefore illegal once µ-gated, and µ-gating is mandatory +because of all-zero padding and inlined IVs. + +## Verification of THIS recovery (2026-07-29) + +`z3_blake_verify.py` runs, and its **structural negative controls all fire +5/5** — `rot_wrong_amount`, `swap_g_operand`, `wrong_iv`, `drop_ff_xor`, +`wrong_msg_index` each SAT as intended. That is strong evidence the recovered +source is the real thing and not a plausible-looking reconstruction. + +**Two fixtures could not be recovered** because they were downloaded or +generated rather than written by a tool call, so they are absent from the +transcript: + +- `blake3-oracle/official_test_vectors.json` — from the upstream BLAKE3 repo +- `blake3-oracle/canonical_6round_vectors.json` — regenerated by running + `test_oracle.py`, which currently stops at the official-vectors anchor + +Consequently the gate's **positive** controls and anchors 1–2 have not been +re-run here. To fully re-validate: fetch the upstream vectors, `pip install +blake3`, run `test_oracle.py` (which writes the canonical vectors), then +re-run `z3_blake_verify.py`. Anchor 3 needs nothing external — Plonky3 is +already vendored at `others/Plonky3/blake3-air`. + +## If this is picked up again + +The blocking item is unchanged and is a **protocol decision, not an +engineering one**: the gate proves the chip matches the reference, *not* that +6 rounds are secure. That needs a named, signed assumption in the spec. + +Note for anyone citing precedent: the EC `lincomb2` design study justified its +own NUMS assumption with "like blake3's 6-round assumption" — but no such +assumption was ever recorded in the spec, because this work never shipped. It +was a precedent for something that had not happened. diff --git a/thoughts/blake3/blake3-chip/DESIGN.md b/thoughts/blake3/blake3-chip/DESIGN.md new file mode 100644 index 000000000..2abae7bcb --- /dev/null +++ b/thoughts/blake3/blake3-chip/DESIGN.md @@ -0,0 +1,411 @@ +# BLAKE3 compression chip — constraint-system & bus design (Phase 2) + +**Status.** Model-level design + z3 equivalence gate, done **before** any Rust. +Ground truth = the phase-1 oracle (`../blake3-oracle/`, VALIDATED against 3 +external anchors). Cost model = the verified one in `../keccak-verify/tier2_cost_model.md` +(a committed cell is expensive; each bus send ≈ 1.5 base cells of aux; **hard** +max constraint degree 3 *including* the ×μ gating factor). + +**Verdict (numbers derived below, gate in `z3_blake_verify.py`):** +* **Layout: B — one row per compression, fully unrolled.** Chosen by arithmetic + (≈5,030 cell-equiv vs ≈5,510 for one-row-per-round), and it deletes the + state+message handoff bus entirely. Table below. +* **O1 (3-operand add carry): option (c) — two summed carry bits.** Cheaper than + both options the oracle listed and stays degree ≤3 after μ-gating. +* **Rotations: rotr16/rotr8 free (byte relabel); rotr12/rotr7 inlined** as the + μ-gated linear shift identity (no HWSL sends), saving 4 sends/G. +* **Every eval constraint is μ-gated, padding is all-zero, every constraint ≤3.** +* **≈5,030 cell-equiv per 6-round compression (≈5,810 for 7-round)** — about + **1/15 of a keccak-f permutation** (≈77,000). + +--- + +## 1. Scope & I/O interface + +The chip implements the compression function `f` (oracle §2.4), **not** the tree. +Primary target is the **6-round internal variant** (Merkle 2-to-1 / Fiat–Shamir); +the design is `ROUNDS`-parameterised so 7-round is the same layout with one more +unrolled round. + +### 1.1 Lean internal interface (the one we build first): 2-to-1 compression + +Exposed on a dedicated **`Blake3` bus**. A parent-node caller supplies the two +child chaining values as the message and reads back the truncated CV. + +**Receive** (multiplicity μ) — the compression inputs: + +| field | words | bytes | source | +|---|---|---|---| +| `h[0..8]` chaining value / key | 8 | 32 | caller | +| `m[0..16]` message = `left_cv ‖ right_cv` | 16 | 64 | caller | +| `t_lo, t_hi` counter split | 2 | 8 | caller (t=0 for parents) | +| `block_len` | 1 | 4 | caller (64 for parents) | +| `flags` | 1 | 4 | caller (PARENT ∣ … for parents) | + +**Send** (multiplicity μ) — the output `out[0..16]` (16 words = 64 B). CV-only +call sites read `out[0:8]`; the chip always produces all 16 (the XOF root needs +them, oracle §2.4). + +`IV[0..4]` (v[8..11]) are **compile-time constants inlined** into the round-0 +arithmetic — not columns, not on the bus. + +### 1.2 General syscall / memory variant (sketched, not built here) + +Same core; replaces the internal `Blake3` receive/send with the keccak I/O +idiom (`prover/src/tables/keccak.rs:160-449`): an `Ecall` receiver binding +(timestamp, syscall#), a `Memw` read of `x10` binding the state pointer, then +per-word `Memw` reads/writes of `h`,`m`,`t`,`block_len`,`flags`,`out`. Adds +~1 Ecall + ~(112+64)/8 ≈ 22 Memw interactions and the pointer-arith columns; +**orthogonal to the mixing core designed here** (open questions O5/O6 live here). + +--- + +## 2. Row-layout decision (by arithmetic) + +Per-compression work (6 rounds): each round = 8 G-functions; each G = **2 +three-operand adds, 2 two-operand adds, 4 XORs, 2 free rotations (rotr16/8), +2 shift rotations (rotr12/7)**. Committed cells and bus sends per G (SSA form, +derivation in §5): + +* committed: **56 byte-cells + 6 carry-bit cells** per G +* sends: **24** per G (16 ByteAlu[XOR] + 8 AreBytes for the two shift rotations) + +| per compression | **A: 1 row / round (6 rows)** | **B: unrolled (1 row)** | +|---|---:|---:| +| logic committed (8 G × 6) | 2,976 | 2,976 | +| feed-forward committed | 64 | 64 | +| I/O input columns | 112 (×6 carried!) = 672 | 112 (once) | +| state+message handoff columns | +128 B/row × 6 = 768 | 0 | +| round-index / selector bookkeeping | ~18 | 0 | +| **committed total** | **≈ 3,760** | **≈ 3,150** | +| bus sends N (logic 192/round) | 1,152 + 6 handoff + 32 msg-rc ≈ 1,190 | 1,152 + 64 ff + 34 I/O = 1,250 | +| **aux = 3·⌈N/2⌉** | **≈ 1,750** | **≈ 1,875** | +| **total cell-equiv** | **≈ 5,510** | **≈ 5,030** | +| handoff bus | `Blake3Round` carries state(64B)+**msg**(64B)/row | none | +| structural cost | per-row state+msg reconstruction, permute-on-bus | pure compile-time wiring | + +**Decision: B.** It wins on total cells (the handoff re-commits the 16-word +state *and* the 16-word message on every one of the 6 rows — BLAKE3, unlike +keccak, must carry the message down the rounds, which is the single biggest +extra cost of A) and it is structurally far simpler: the message schedule is a +compile-time permutation, so unrolling makes every round reference the original +16 committed message words under `permute^r` with **zero** runtime handoff. The +concentration of all sends into one row makes B's aux marginally higher, but the +committed-column saving dominates. B also removes round-index bookkeeping and the +`Blake3Round` bus wholesale. (Matches the oracle's recommendation, now with the +numbers behind it.) + +Only reason to revisit A: if the ~3,150-wide single row's LDE/Merkle width ever +dominates trace area for tiny proofs — not the case here (keccak's per-row width +is already ~1,480+aux and BLAKE3 has 1/4 the rounds). + +--- + +## 3. Column layout (Layout B) + +One row = one compression call. Names group by role; counts are for `ROUNDS=6`. +"SSA word" = a fresh 4-byte committed word produced by one op. + +| block | columns | count | notes | +|---|---|---:|---| +| `TIMESTAMP_0/1` | 2 | 2 | bus binding (internal variant may omit) | +| `MU` | 1 | 1 | multiplicity / gate flag | +| `H[0..8]` | 8 words | 32 | input CV bytes | +| `M[0..16]` | 16 words | 64 | input message bytes | +| `T_LO,T_HI,BLEN,FLAGS` | 4 words | 16 | counter split, block_len, flags | +| per-G logic × 48 G | see §5 | 2,976 | add/xor/shift SSA words + carry bits | +| feed-forward `OUT[0..16]` | 16 words | 64 | XOR outputs | +| **main columns total** | | **≈ 3,155** | | +| aux (LogUp) `= 3·⌈1250/2⌉` | | **1,875** | degree-3 ext columns | + +Per-G committed breakdown (each of the 48 G-instances): + +| sub-op | SSA output | bytes | carry bits | +|---|---|---:|---:| +| `add3` v[a]=v[a]+v[b]+mx | `A1` | 4 | 2 | +| `xor` v[d]^v[a] (→rotr16 free) | `X1` | 4 | – | +| `add2` v[c]+v[d] | `C1` | 4 | 1 | +| `xor` v[b]^v[c] | `X2` | 4 | – | +| `rotr12`(X2) | `SLLlo,SLLClo,SLLhi,SLLChi,B1` | 12 | – | +| `add3` v[a]=v[a]+v[b]+my | `A2` | 4 | 2 | +| `xor` v[d]^v[a] (→rotr8 free) | `X3` | 4 | – | +| `add2` v[c]+v[d] | `C2` | 4 | 1 | +| `xor` v[b]^v[c] | `X4` | 4 | – | +| `rotr7`(X4) | `SLLlo,SLLClo,SLLhi,SLLChi,B2` | 12 | – | +| **per G** | | **56** | **6** | + +`rotr16`/`rotr8` produce **no columns** — the next consumer reads the XOR-output +bytes in relabeled order (see §4.2). + +--- + +## 4. Constraints & bus interactions + +All arithmetic reduces to the existing precomputed-BITWISE receivers +(`prover/src/tables/bitwise.rs`); all eval constraints are **μ-gated**, so +degree = (μ:1) × (body). Padding rows are all-zero and μ=0. + +### 4.1 XOR — `ByteAlu[XOR]` send (per byte) + +For each 32-bit XOR, 4 sends `ByteAlu[XOR, a_byte, b_byte] → out_byte` +(`bitwise.rs:903`). The lookup **simultaneously** byte-range-checks both operands +and pins `out` to the exact XOR — no separate range check. Operands may be linear +combos (the byte contract requires `sum ≤ 255`), which lets a free rotation be +read in-place. Eval constraints: none (pure lookup). Degree: n/a. + +### 4.2 Rotations + +* **rotr16 / rotr8 — free.** rotr16 = byte relabel `[b0,b1,b2,b3]→[b2,b3,b0,b1]`; + rotr8 = `[b1,b2,b3,b0]` (oracle §3.1, exhaustively verified). No columns, no + lookups, no constraints — the consumer indexes the source XOR's bytes in + rotated order. +* **rotr12 / rotr7 — inline shift identity (chosen over HWSL sends).** + `rotr12 = rotl20 = rotl16∘rotl4` (inner `r=4`); `rotr7 = rotl25 = rotl16∘rotl9` + (`r=9`). For input word `X = xlo + 2^16·xhi` (halfwords `xlo,xhi`, 2 bytes each): + + **Shift identities (eval, degree 2 after ×μ):** + ``` + μ·( xlo·2^r − SLLC_lo·2^16 − SLL_lo ) = 0 + μ·( xhi·2^r − SLLC_hi·2^16 − SLL_hi ) = 0 + ``` + **Recombine + halfword swap (eval, degree 2 after ×μ):** + ``` + μ·( Ylo − SLL_hi − SLLC_lo ) = 0 # output low halfword = Y[0]+256·Y[1] + μ·( Yhi − SLL_lo − SLLC_hi ) = 0 # output high halfword = Y[2]+256·Y[3] + ``` + **Range checks (sends):** `AreBytes` on the 8 bytes of `SLL_lo,SLLC_lo,SLL_hi, + SLLC_hi` = 4 sends/rotation (`bitwise.rs:783`). `Y` is range-checked *free* by + the downstream XOR that consumes it. + + Soundness (proven in `../keccak-verify/hwsl_inline_test.py` Part 2, and by the + width audit in the gate): given `SLL_* ∈ [0,2^16)` (the tight remainder bound + from AreBytes) and `2^16` invertible mod p, the identity **uniquely** pins + `SLL = (xlo·2^r) mod 2^16` and `SLLC = (xlo·2^r) >> 16`; the loose 16-bit bound + on `SLLC` suffices because it is the quotient, not the remainder. The two + recombination sums are over non-overlapping bit ranges, so `+` = `OR` and each + is an exact 16-bit halfword. + + **HWSL alternative, priced:** replace each shift identity with an `Hwsl` send + (`bitwise.rs:831`). Cost/rotation: +2 Hwsl sends, same AreBytes, same columns. + Per compression that is +4 sends/G × 48 = +192 sends → +288 aux cells (≈6%). + Inline wins because the eval identity is free of columns/sends; it costs only + degree budget (2 ≤ 3). **Use inline.** + +### 4.3 Two-operand add — `emit_add_pair` low half (eval, degree 3 after ×μ) + +`s = (a+b) mod 2^32`; one carry bit. Following `templates.rs:334`: +``` +carry = (a + b − s)·2^-32 # linear expression, INV_SHIFT_32 = (2^32)^-1 +μ · carry·(1 − carry) = 0 # degree (1)×(1)×(1 body)=2, ×μ = 3 +``` +`s`'s bytes are range-checked **free** by the next XOR that consumes `s` +(every add output in G feeds a subsequent XOR — see §5). Booleanity + `s∈[0,2^32)` +⇒ `s` unique. + +### 4.4 Three-operand add — **O1 resolved: option (c), two summed carry bits** + +`s = (a+b+m) mod 2^32`, carry ∈ {0,1,2}. Commit two carry **bits** `c1,c2` +(2 cells, no intermediate word): +``` +μ·( a + b + m − s − 2^32·(c1+c2) ) = 0 # sum identity, linear → ×μ = degree 2 +μ · c1·(1 − c1) = 0 # ×μ = degree 3 +μ · c2·(1 − c2) = 0 # ×μ = degree 3 +``` +`s`'s bytes range-checked free downstream. `c1+c2 ∈ {0,1,2}` covers the carry; +`s∈[0,2^32)` + the sum identity pin `s = (a+b+m) mod 2^32` uniquely (proof in the +gate's width audit). + +**Why (c):** + +| O1 option | extra committed / 3-op add | degree (ungated → ×μ) | legal under ×μ? | +|---|---|---|---| +| (a) one ternary carry `k(k−1)(k−2)=0` | 1 bit | 3 → **4** | ❌ (μ-gating mandatory, §4.5) | +| (b) two chained binary adds | 1 word (4 B) + 2 AreBytes | 2 → 3 | ✅ but +4B +2 sends | +| **(c) two summed carry bits** | **2 bits** | 2 (bool) / 1 (sum) → 3 / 2 | ✅ **cheapest** | + +Over a compression, (c) vs (b): saves (4B−2bit) per 3-op add × 96 three-op adds +≈ **300 committed cells + 192 AreBytes sends**. (c) is a strict refinement of the +oracle's two options. + +### 4.5 μ-gating & padding — **O2 resolved: gate everything, all-zero padding** + +Every eval constraint is multiplied by `μ` (the `MU` column, 1 on the real row, +0 on padding), exactly like `keccak_rnd`'s IS_BIT (`keccak_rnd.rs:914`). Padding +rows are **all-zero**: +* bus interactions carry `Multiplicity::Column(MU)` ⇒ 0 contribution on padding; +* eval constraints are `μ·(…)` ⇒ 0 on padding regardless of the (zero) cells. + +This is why O1 must be (b) or (c): the ternary carry (a) is degree 3 *ungated*, +and ×μ pushes it to 4. Inlined `IV` constants are fine because the round-0 add +that consumes them is itself μ-gated (its carry expression is nonsense on an +all-zero padding row, but ×μ=0 kills it). **The μ-gating requirement is what +forecloses option (a) — this is the single tightest coupling in the design.** + +### 4.6 Feed-forward (16 XORs, all `ByteAlu[XOR]`) + +``` +out[i] = v[i] ⊕ v[i+8] i = 0..8 (v[i+8] = final state word) +out[i+8] = v[i+8] ⊕ h[i] i = 0..8 (h = original input CV column) +``` +64 sends, 64 committed output bytes (the XOR outputs), range-checked free by the +lookup. Output bytes are shipped on the `Blake3` send. + +### 4.7 Range checks that are NOT free + +The message `m` enters **only** through adds (never XORed), so its 64 bytes need +explicit `AreBytes` (32 sends/compression). `h` and `t/block_len/flags` all feed +an XOR (feed-forward / round-0 diagonal), so they are free. Every add/shift/xor +output feeds a downstream XOR ⇒ free. + +### 4.8 Degree ledger (the hard gate) + +| constraint | body degree | × μ | ≤ 3? | +|---|---:|---:|:--:| +| 2-op add carry booleanity | 2 | 3 | ✅ | +| 3-op add sum identity | 1 | 2 | ✅ | +| 3-op add carry booleanity ×2 | 2 | 3 | ✅ | +| shift identity (×2) | 1 | 2 | ✅ | +| recombine identity (×2) | 2 | 3 | ✅ | +| (rejected) ternary carry | 3 | **4** | ❌ | + +Worst legal constraint = 3. **No constraint exceeds 3.** + +--- + +## 5. Per-G dataflow, SSA + free range-checks + +``` +A1 = add3(v[a], v[b], mx) # v[a] ; 2 carry bits ; range-checked by X1 +X1 = xor(v[d], A1) ; v[d] = rotr16(X1) # free relabel +C1 = add2(v[c], v[d]=rotr16(X1)) # v[c] ; 1 carry bit ; range-checked by X2 +X2 = xor(v[b], C1) +B1 = rotr12(X2) # v[b] ; range-checked by X4 / next round +A2 = add3(A1, B1, my) # v[a] ; 2 carry bits ; range-checked by X3 +X3 = xor(v[d]=rotr16(X1), A2) ; v[d]=rotr8(X3) +C2 = add2(C1, v[d]=rotr8(X3)) # v[c] ; 1 carry bit ; range-checked by X4 +X4 = xor(B1, C2) +B2 = rotr7(X4) # v[b] ; range-checked next round / FF +``` +Every committed add/shift word is an operand of a later XOR ⇒ its bytes are +byte-range-checked for free by that `ByteAlu` lookup. Confirmed: no add/shift +output needs its own AreBytes. (Only `m` does — §4.7.) + +--- + +## 6. Cost & comparison + +| quantity (6-round) | value | +|---|---:| +| committed main columns | ≈ 3,150 | +| bus sends N | ≈ 1,250 (832 XOR incl. 64 feed-forward + 384 shift-AreBytes + 32 msg-AreBytes + 2 I/O) | +| aux base cells (3·⌈N/2⌉) | ≈ 1,875 | +| **total cell-equiv / compression** | **≈ 5,030** | +| 7-round variant | ≈ 5,810 | +| keccak-f permutation (reference) | ≈ 77,000 | +| **BLAKE3-6r as fraction of keccak-f** | **≈ 1/15 (6.5%)** | + +Dominated by the ~960 byte-XOR lookups, as the oracle predicted. Note: the +oracle's prose "¼–⅓ of a keccak permutation" is inconsistent with its own +5–6k/compression figure; the detailed count here (≈5k vs 77k) puts it at **~1/15**. + +--- + +## 7. Soundness-critical spots a Rust implementation must NOT deviate from + +1. **μ-gate every eval constraint** (carry booleanity, sum identity, shift + identity, recombine). Un-gated ternary carry or an un-gated constraint with + inlined IV constants breaks all-zero padding. (§4.5) +2. **3-op add = two summed carry bits with the explicit sum identity** — not a + single ternary carry (degree 4 after gating), and the sum identity must be + present (without it, `s` is only constrained mod nothing). (§4.4) +3. **Shift identity needs the tight `SLL ∈ [0,2^16)` AreBytes bound**; dropping it + makes the rotation forgeable (a wrong `SLL` admits a large field `SLLC`). + Soundness relies on `2^16` invertible mod p — a BV model cannot see this; + verify in the field (gate width audit + `hwsl_inline_test.py`). (§4.2) +4. **Every add/shift output must actually feed a downstream XOR** (its only range + check). If a future refactor reorders so an add output is *last* with no XOR + consumer, add an explicit AreBytes or the carry argument is unsound. (§5) +5. **Message `m` needs explicit AreBytes** — it is never XORed. (§4.7) +6. **rotr16/rotr8 byte order** exactly `[b2,b3,b0,b1]` / `[b1,b2,b3,b0]` + (little-endian). A wrong relabel silently corrupts. (§4.2) +7. **Message permutation `permute^r`** wired per round from the *original* 16 + `M` columns; MSG_PERMUTATION = `[2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]`. The + trailing permute after the last round is unused (oracle §2.4). (Gate control + `wrong_msg_index`.) +8. **IV / feed-forward / counter split** exactly per oracle §2.4: + `v[8..12]=IV[0..4]` inlined, `v[12]=t_lo, v[13]=t_hi, v[14]=block_len, + v[15]=flags`; `out[i]=v[i]⊕v[i+8]`, `out[i+8]=v[i+8]⊕h[i]`. (Controls + `wrong_iv`, `drop_ff_xor`.) +9. **Non-overflow side conditions (width audit):** all add/shift field + expressions stay `< 2^35 ≪ p`, so `≡0 mod p` ⇒ `=0` as integers; the whole + soundness argument depends on operands being genuine ≤32-bit (byte columns) + and carries being genuine bits. + +--- + +## 8. Gate + +`z3_blake_verify.py` — free-variable model of every column, every lookup/eval +constraint as an equation, `assert output ≠ oracle-reference`, ask z3 for a +counterexample. Reference (`bref_*`) is an independent 32-bit-BV port of +`blake3_ref.py` (RotateRight / + / ^), structurally independent of the byte-level +shift wiring. Results are appended to §9 after the run (`run.log`). +``` +python3 z3_blake_verify.py # round + wrapper + controls + audit (fast) +python3 z3_blake_verify.py --full # + monolithic 6- and 7-round UNSAT +``` + +## 9. Gate results + +Default run (`python3 z3_blake_verify.py`, ~2 min) — **OVERALL: PASS**: + +| check | result | meaning | +|---|---|---| +| **MAIN 0** — one G-function, free inputs | **UNSAT** | the quarter-round (byte-XOR + inline rotr12/rotr7 shift identities + 2-op & 3-op adds) is correctly & tightly constrained; **covers every G, hence every round** (a round is a fixed composition of 8 G-calls). | +| **MAIN 1** — init-state + feed-forward (rounds=0) | **UNSAT** | `v` layout (`h`/IV/counter-split/block_len/flags) and `out[i]=v[i]⊕v[i+8]`, `out[i+8]=v[i+8]⊕h[i]` are correct. | +| neg `rot_wrong_amount` | **SAT** | wrong rotation amount detected. | +| neg `swap_g_operand` | **SAT** | swapped G-function operand detected. | +| neg `wrong_iv` | **SAT** | wrong IV constant detected. | +| neg `drop_ff_xor` | **SAT** | dropped feed-forward XOR detected. | +| neg `wrong_msg_index` | **SAT** | wrong message-schedule index detected (permutation is load-bearing). | +| **pos** 6-round seeds 0,1,2 (canonical vectors) | **SAT** | full 6-round pipeline reproduces the oracle's recorded output for concrete inputs. | +| **pos** 7-round (oracle-generated) | **SAT** | full 7-round pipeline reproduces the oracle's `compress(…,rounds=7)`. | +| audit: shift `SLL` 16-bit bound present | **UNSAT** | with AreBytes the shift output is pinned. | +| audit: **DROP `SLL` bound** (field neg ctrl) | **SAT** | without it the rotation is forgeable (needs `2^16` invertible mod p). | +| audit: 3-add carry booleanity present | **UNSAT** | with booleanity the sum `s` is pinned. | +| audit: **DROP carry booleanity** (field neg ctrl #4) | **SAT** | without it `s` is forgeable in the prime field. | + +**The 6th team-lead control — "dropped carry booleanity" — lives in the width +audit, not the BV controls, and this is correct.** Dropping a committed carry +column's booleanity is a *field-level* soundness bug: the column becomes a full +Goldilocks element, but a *bounded-BV* model keeps the 8-bit carry + `s∈[0,2^32)` +byte-range, which still pins `s`, so BV reports UNSAT (verified: the BV version +does). Only the mod-p model exhibits the forgery — exactly the phenomenon +`../keccak-verify/hwsl_inline_test.py` Part 2 documents (`2^16`/`2^32` are zero +divisors mod `2^n`). The gate deliberately separates BV-observable logic bugs +from field-only soundness bugs; both classes fire. + +**`--full`** additionally runs the heavy monolithic symbolic UNSATs (one round; +compression rounds=2 for the permutation; full 6- and 7-round). These are *bonus* +confirmations — the G-unsat + fixed-composition chaining argument + rounds=0 + +the concrete full-pipeline positive controls already establish full-compression +correctness. (The direct 6-round symbolic UNSAT is large; it is not required for +the verdict and may take a long time / be run offline.) + +### What is and isn't proven +* **Proven (symbolic, all inputs):** the G quarter-round; the init-state layout; + the feed-forward — hence, by the chaining argument, the full N-round + compression for **both ROUNDS=6 and ROUNDS=7**. +* **Proven (concrete, external anchor):** the *entire* unrolled pipeline + (init + 6/7 rounds + message permutation + feed-forward) reproduces the + oracle's validated vectors. +* **Proven (field-level):** the AreBytes shift bound and the add-carry booleanity + are each *necessary* (dropping either is a forgery mod p). +* **Assumed (assume-guarantee, not re-proven here):** the precomputed BITWISE + table contracts themselves (ByteAlu[XOR], AreBytes) — these are existing, + separately-audited chips (`prover/src/tables/bitwise.rs`). Same assumption the + keccak gate makes. +* **Not modeled here:** the memory/syscall I/O variant (§1.2) — orthogonal; + open questions O5 (counter width, already covered by the Plonky3 anchor) and + O6 (endianness at the MEMW boundary) live there and must be pinned when that + interface is wired. diff --git a/thoughts/blake3/blake3-chip/z3_blake_verify.py b/thoughts/blake3/blake3-chip/z3_blake_verify.py new file mode 100644 index 000000000..f6a439f98 --- /dev/null +++ b/thoughts/blake3/blake3-chip/z3_blake_verify.py @@ -0,0 +1,561 @@ +""" +Formal (z3 / QF_BV) assume-guarantee gate for the BLAKE3 compression chip design. + +Method (mirrors ../keccak-verify/z3_verify.py): + * Every committed column of the designed chip is a FREE bitvector. + * Every bus lookup (under its precomputed-table contract) and every eval + constraint becomes an equation relating those free vars. + * The chip OUTPUT is whatever the constraints force. We assert + `output != reference(input)` and ask z3 for a counterexample: + UNSAT -> for all constraint-satisfying assignments, output == reference + (the chip is correctly & tightly constrained). + SAT -> the constraints permit a wrong output (under-constrained / mis-wired). + +The reference (`bref_*`) is written directly from the BLAKE3 spec with 32-bit +BV ops (RotateRight / + / ^) — structurally INDEPENDENT of the chip's byte-level +XOR / halfword-shift wiring, exactly like keccak's zref_round vs the byte circuit. + +Chip contracts assumed (assume-guarantee, from prover/src/tables/bitwise.rs): + ByteAlu[XOR](a,b)->c : a,b,c are bytes and c = a ^ b. (8-bit width = byte + range-check; output pinned by the precomputed table.) + AreBytes[a,b] : a,b are bytes (8-bit width). + (HWSL is NOT used: rotations are inlined as the mu-gated linear shift identity + in*2^r == SLLC*2^16 + SLL, whose soundness is proven by ../keccak-verify/ + hwsl_inline_test.py given the AreBytes 16-bit bounds + 2^16 invertible mod p.) + +Add carries and shift decompositions are eval constraints (mu-gated, degree <=3); +here mu=1 (a real row), so mu drops out and we model the ungated equation. + +DESIGN DECISIONS UNDER TEST (see DESIGN.md): + * State stored as bytes; XOR byte-wise via ByteAlu[XOR]. + * rotr16 / rotr8 : FREE byte relabels (no columns, no lookups). + * rotr12 / rotr7 : inner rotl r=4 / r=9 -> two halfword shift-identities + + cross-halfword recombine + halfword swap. + * 2-operand add : one carry bit, a+b == s + 2^32*carry, s range-checked. + * 3-operand add : O1 option (c) -- TWO summed carry bits c1,c2 in {0,1}, + a+b+m == s + 2^32*(c1+c2). (No committed intermediate word; + degree stays <=3 after mu-gating, unlike k(k-1)(k-2).) +""" +import sys +import json +import os +from z3 import ( + BitVec, BitVecVal, Concat, ZeroExt, RotateRight, Or, And, Solver, sat, unsat, + Int, IntVal, +) + +# --------------------------------------------------------------------------- +# BLAKE3 constants (spec; cross-checked against Plonky3 in the oracle) +# --------------------------------------------------------------------------- +IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] +MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +MASK32 = 0xFFFFFFFF +WIDE = 48 # wide BV width for add / shift identities (honest < 2^35 << 2^48) +P = 2**64 - 2**32 + 1 # Goldilocks prime (used in the width-audit field checks) + +# G-function schedule: (a,b,c,d, mx_index, my_index) for the 8 calls of a round. +G_CALLS = [ + (0, 4, 8, 12, 0, 1), + (1, 5, 9, 13, 2, 3), + (2, 6, 10, 14, 4, 5), + (3, 7, 11, 15, 6, 7), + (0, 5, 10, 15, 8, 9), + (1, 6, 11, 12, 10, 11), + (2, 7, 8, 13, 12, 13), + (3, 4, 9, 14, 14, 15), +] + +# =========================================================================== +# Independent z3-native reference (BLAKE3 spec, 32-bit BV words) +# =========================================================================== +def bref_g(v, a, b, c, d, mx, my): + v[a] = v[a] + v[b] + mx + v[d] = RotateRight(v[d] ^ v[a], 16) + v[c] = v[c] + v[d] + v[b] = RotateRight(v[b] ^ v[c], 12) + v[a] = v[a] + v[b] + my + v[d] = RotateRight(v[d] ^ v[a], 8) + v[c] = v[c] + v[d] + v[b] = RotateRight(v[b] ^ v[c], 7) + + +def bref_round(v, m): + for (a, b, c, d, ix, iy) in G_CALLS: + bref_g(v, a, b, c, d, m[ix], m[iy]) + + +def bref_permute(m): + return [m[MSG_PERMUTATION[i]] for i in range(16)] + + +def bref_round_only(state16, msg16): + """One round, free 16-word state + free 16-word message -> new state.""" + v = list(state16) + bref_round(v, msg16) + return v + + +def bref_compress(h, m, tlo, thi, bl, fl, rounds): + """Full compression. h:8 BV32, m:16 BV32, counter split tlo/thi, bl, fl.""" + v = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], + BitVecVal(IV[0], 32), BitVecVal(IV[1], 32), + BitVecVal(IV[2], 32), BitVecVal(IV[3], 32), + tlo, thi, bl, fl] + schedule = list(m) + for r in range(rounds): + bref_round(v, schedule) + if r < rounds - 1: + schedule = bref_permute(schedule) + out = [None] * 16 + for i in range(8): + out[i] = v[i] ^ v[i + 8] + out[i + 8] = v[i + 8] ^ h[i] + return out + + +# =========================================================================== +# Chip circuit model. A "word" is a list of 4 free 8-bit BVs [b0,b1,b2,b3] +# (little-endian). Byte width == the ByteAlu/AreBytes range-check contract. +# =========================================================================== +class Circuit: + def __init__(self, tag, bug=None): + self.C = [] + self.tag = tag + self.bug = bug + self.n = 0 + + def _fresh(self, w=8): + v = BitVec(f"{self.tag}_v{self.n}", w) + self.n += 1 + return v + + def fresh_word(self): + return [self._fresh(8) for _ in range(4)] + + def const_word(self, val): + return [BitVecVal((val >> (8 * i)) & 0xFF, 8) for i in range(4)] + + # -- value helpers ----------------------------------------------------- + def wval(self, word): + """word as a WIDE-bit BV integer (little-endian byte combination).""" + acc = BitVecVal(0, WIDE) + for i in range(4): + acc = acc + ZeroExt(WIDE - 8, word[i]) * BitVecVal(1 << (8 * i), WIDE) + return acc + + def hwval(self, blo, bhi): + """halfword (2 bytes) as a WIDE-bit BV.""" + return ZeroExt(WIDE - 8, blo) + ZeroExt(WIDE - 8, bhi) * BitVecVal(256, WIDE) + + def word32(self, word): + return Concat(word[3], word[2], word[1], word[0]) + + def fresh_bit(self, boolean=True): + v = self._fresh(8) + if boolean: + self.C.append(Or(v == 0, v == 1)) # mu-gated IS_BIT (mu=1 here) + return v + + # -- operations under contract ---------------------------------------- + def xor(self, A, B): + """ByteAlu[XOR]: out byte-wise = A ^ B (auto byte range-check).""" + out = self.fresh_word() + for i in range(4): + self.C.append(out[i] == A[i] ^ B[i]) + return out + + def rotr16(self, A): + # rotate-right 16 == swap halfwords == byte relabel [b2,b3,b0,b1]. FREE. + return [A[2], A[3], A[0], A[1]] + + def rotr8(self, A): + # rotate-right 8 == byte relabel [b1,b2,b3,b0]. FREE. + return [A[1], A[2], A[3], A[0]] + + def add2(self, A, B, drop_bool=False): + """2-operand add mod 2^32: a+b == s + 2^32*carry, carry in {0,1}.""" + s = self.fresh_word() + carry = self.fresh_bit(boolean=not drop_bool) + self.C.append( + self.wval(A) + self.wval(B) + == self.wval(s) + ZeroExt(WIDE - 8, carry) * BitVecVal(1 << 32, WIDE) + ) + return s + + def add3(self, A, B, M, drop_bool=False): + """3-operand add mod 2^32 (O1 option c): TWO summed carry bits. + a+b+m == s + 2^32*(c1+c2), c1,c2 in {0,1}.""" + s = self.fresh_word() + c1 = self.fresh_bit(boolean=not drop_bool) + c2 = self.fresh_bit(boolean=not drop_bool) + csum = ZeroExt(WIDE - 8, c1) + ZeroExt(WIDE - 8, c2) + self.C.append( + self.wval(A) + self.wval(B) + self.wval(M) + == self.wval(s) + csum * BitVecVal(1 << 32, WIDE) + ) + return s + + def rotr(self, A, n, wrong_amount=False): + """rotr12 / rotr7 via inner rotl r + halfword swap. + + r=4 for n=12 (rotl20=rotl16.rotl4); r=9 for n=7 (rotl25=rotl16.rotl9). + Shift identity (inline, mu-gated): hw*2^r == SLLC*2^16 + SLL, with SLL + the tight 16-bit remainder and SLLC the (loose 16-bit) quotient. Then + Y_lo = SLL_hi + SLLC_lo, Y_hi = SLL_lo + SLLC_hi (non-overlapping adds). + """ + r = {12: 4, 7: 9}[n] + if wrong_amount: + r += 1 # negative control: wrong rotation amount + xlo = self.hwval(A[0], A[1]) + xhi = self.hwval(A[2], A[3]) + # SLL / SLLC as free halfwords (each = 2 free bytes -> AreBytes 16-bit). + sll_lo = self.fresh_word()[:2] + sllc_lo = self.fresh_word()[:2] + sll_hi = self.fresh_word()[:2] + sllc_hi = self.fresh_word()[:2] + SLL_lo, SLLC_lo = self.hwval(*sll_lo), self.hwval(*sllc_lo) + SLL_hi, SLLC_hi = self.hwval(*sll_hi), self.hwval(*sllc_hi) + two_r = BitVecVal(1 << r, WIDE) + two_16 = BitVecVal(1 << 16, WIDE) + # shift identities + self.C.append(xlo * two_r == SLLC_lo * two_16 + SLL_lo) + self.C.append(xhi * two_r == SLLC_hi * two_16 + SLL_hi) + # recombine (rotl_r) + halfword swap (rotl16) + Y = self.fresh_word() + self.C.append(self.hwval(Y[0], Y[1]) == SLL_hi + SLLC_lo) # Y low halfword + self.C.append(self.hwval(Y[2], Y[3]) == SLL_lo + SLLC_hi) # Y high halfword + return Y + + +# --------------------------------------------------------------------------- +# Build one round of the chip (free input state + free message). +# --------------------------------------------------------------------------- +def build_g(cir, v, a, b, c, d, mx, my, bug, gflag): + b_first = c if (bug == "swap_g_operand" and gflag) else b # WRONG: v[c] for v[b] + v[a] = cir.add3(v[a], v[b_first], mx) + v[d] = cir.rotr16(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) + v[b] = cir.rotr(cir.xor(v[b], v[c]), 12, + wrong_amount=(bug == "rot_wrong_amount" and gflag)) + v[a] = cir.add3(v[a], v[b], my, + drop_bool=(bug == "drop_carry_bool" and gflag)) + v[d] = cir.rotr8(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) + v[b] = cir.rotr(cir.xor(v[b], v[c]), 7) + + +def build_round(cir, v, m, bug=None, bug_first_g_only=True): + for gi, (a, b, c, d, ix, iy) in enumerate(G_CALLS): + gflag = (gi == 0) if bug_first_g_only else True + build_g(cir, v, a, b, c, d, m[ix], m[iy], bug, gflag) + + +def build_compress(cir, h, m, tlo, thi, bl, fl, rounds, bug=None): + iv = list(IV) + if bug == "wrong_iv": + iv[0] ^= 1 # negative control + v = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], + cir.const_word(iv[0]), cir.const_word(iv[1]), + cir.const_word(iv[2]), cir.const_word(iv[3]), + tlo, thi, bl, fl] + perm = list(MSG_PERMUTATION) + if bug == "wrong_msg_index": + perm[0], perm[1] = perm[1], perm[0] # negative control + schedule = list(m) + for r in range(rounds): + # only inject round-logic bugs in round 0's first G + rbug = bug if (r == 0 and bug in + ("rot_wrong_amount", "swap_g_operand", "drop_carry_bool")) else None + build_round(cir, v, schedule, bug=rbug) + if r < rounds - 1: + schedule = [schedule[perm[i]] for i in range(16)] + out = [None] * 16 + for i in range(8): + out[i] = cir.xor(v[i], v[i + 8]) + out[i + 8] = cir.xor(v[i + 8], h[i]) + if bug == "drop_ff_xor" and i == 0: + out[0] = cir.fresh_word() # dropped: output left free + return out + + +# =========================================================================== +# Checks +# =========================================================================== +def check_g(bug=None, timeout_ms=0): + """Single G-function vs reference G. Free 4 state words + 2 message words. + UNSAT = the G quarter-round is correctly & tightly constrained. A round is a + fixed composition of 8 G-calls on specified indices, so a correct G under + arbitrary inputs => correct round (the chaining argument).""" + tag = "g" + (f"_{bug}" if bug else "") + cir = Circuit(tag, bug) + va, vb, vc, vd = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + mx, my = cir.fresh_word(), cir.fresh_word() + v = [None] * 16 + v[0], v[1], v[2], v[3] = va, vb, vc, vd + build_g(cir, v, 0, 1, 2, 3, mx, my, bug, gflag=True) + rv = [cir.word32(va), cir.word32(vb), cir.word32(vc), cir.word32(vd)] + bref_g(rv, 0, 1, 2, 3, cir.word32(mx), cir.word32(my)) + s = Solver() + if timeout_ms: + s.set("timeout", timeout_ms) + s.add(And(*cir.C)) + s.add(Or(cir.word32(v[0]) != rv[0], cir.word32(v[1]) != rv[1], + cir.word32(v[2]) != rv[2], cir.word32(v[3]) != rv[3])) + return s.check() + + +def check_round(bug=None, timeout_ms=0): + """Round circuit vs reference round. Free state + free message. UNSAT = correct.""" + tag = "rnd" + (f"_{bug}" if bug else "") + cir = Circuit(tag, bug) + state = [cir.fresh_word() for _ in range(16)] + msg = [cir.fresh_word() for _ in range(16)] + v = list(state) + build_round(cir, v, msg, bug=bug) + ref = bref_round_only([cir.word32(w) for w in state], + [cir.word32(w) for w in msg]) + s = Solver() + if timeout_ms: + s.set("timeout", timeout_ms) + s.add(And(*cir.C)) + s.add(Or(*[cir.word32(v[i]) != ref[i] for i in range(16)])) + return s.check() + + +def check_compress(rounds, bug=None, timeout_ms=0): + """Full compression vs reference. UNSAT = correct.""" + tag = f"cmp{rounds}" + (f"_{bug}" if bug else "") + cir = Circuit(tag, bug) + h = [cir.fresh_word() for _ in range(8)] + m = [cir.fresh_word() for _ in range(16)] + tlo, thi, bl, fl = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + out = build_compress(cir, h, m, tlo, thi, bl, fl, rounds, bug=bug) + ref = bref_compress([cir.word32(w) for w in h], [cir.word32(w) for w in m], + cir.word32(tlo), cir.word32(thi), cir.word32(bl), + cir.word32(fl), rounds) + s = Solver() + if timeout_ms: + s.set("timeout", timeout_ms) + s.add(And(*cir.C)) + s.add(Or(*[cir.word32(out[i]) != ref[i] for i in range(16)])) + return s.check() + + +def positive_control_compress(rounds, h_i, m_i, tlo_i, thi_i, bl_i, fl_i, out_i): + """Non-vacuity + external anchor: pin inputs to a concrete oracle vector, + assert the chip output == the RECORDED oracle output, expect SAT.""" + tag = f"pos{rounds}" + cir = Circuit(tag) + h = [cir.fresh_word() for _ in range(8)] + m = [cir.fresh_word() for _ in range(16)] + tlo, thi, bl, fl = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + out = build_compress(cir, h, m, tlo, thi, bl, fl, rounds) + s = Solver() + s.add(And(*cir.C)) + # pin inputs + for wi, val in zip(h, h_i): + s.add(cir.word32(wi) == BitVecVal(val, 32)) + for wi, val in zip(m, m_i): + s.add(cir.word32(wi) == BitVecVal(val, 32)) + s.add(cir.word32(tlo) == BitVecVal(tlo_i, 32)) + s.add(cir.word32(thi) == BitVecVal(thi_i, 32)) + s.add(cir.word32(bl) == BitVecVal(bl_i, 32)) + s.add(cir.word32(fl) == BitVecVal(fl_i, 32)) + # pin output to the recorded oracle vector + for wi, val in zip(out, out_i): + s.add(cir.word32(wi) == BitVecVal(val, 32)) + return s.check() + + +# =========================================================================== +# WIDTH AUDIT: field-level (mod p) bound-necessity for the shift identity and +# the add carry. A wide-BV model cannot show these (2^16 / 2^32 are zero +# divisors mod 2^n); the prime field is required, exactly as +# ../keccak-verify/hwsl_inline_test.py Part 2 demonstrates. +# =========================================================================== +def field_shift_bound(r, in_hw, drop_sll_bound): + """hw*2^r == SLLC*2^16 + SLL (mod p). SLL bounded to [0,2^16) unless dropped. + Returns 'unsat' if SLL is pinned to the honest value; 'sat' if ambiguous.""" + s = Solver() + if drop_sll_bound: + SLL = Int("SLL"); s.add(SLL >= 0, SLL < P) # UNBOUNDED field elt + else: + lo, hi = Int("sll_lo"), Int("sll_hi") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) # AreBytes: 2 bytes + SLL = lo + 256 * hi + SLLC = Int("SLLC") + s.add(SLLC >= 0, SLLC < 2**16) # loose 16-bit is fine + s.add((in_hw * (2 ** r) - SLLC * (2 ** 16) - SLL) % P == 0) + sll_ref = (in_hw * (2 ** r)) % (2 ** 16) + s.add(SLL != sll_ref) # a WRONG SLL admissible? + return str(s.check()) + + +def field_add_carry(a, b, m3, drop_bool): + """3-op: a+b+m == s + 2^32*(c1+c2) (mod p). s in [0,2^32). carries in {0,1} + unless dropped. Returns 'unsat' if s pinned to honest, 'sat' if ambiguous.""" + s = Solver() + S = Int("S"); s.add(S >= 0, S < 2**32) + if drop_bool: + c1 = Int("c1"); s.add(c1 >= 0, c1 < P) # UNBOUNDED + csum = c1 + else: + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + csum = c1 + c2 + s.add((a + b + m3 - S - (2**32) * csum) % P == 0) + s_ref = (a + b + m3) % (2**32) + s.add(S != s_ref) + return str(s.check()) + + +# =========================================================================== +def load_canonical_6round(): + here = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(here, "..", "blake3-oracle", "canonical_6round_vectors.json") + with open(path) as f: + return json.load(f) + + +def gen_7round_vector(): + """Concrete 7-round compression vector from the validated oracle itself.""" + here = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, os.path.join(here, "..", "blake3-oracle")) + import blake3_ref as ora + import random + rng = random.Random(12345) + h = [rng.randrange(0, 1 << 32) for _ in range(8)] + m = [rng.randrange(0, 1 << 32) for _ in range(16)] + t = rng.randrange(0, 1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + out = ora.compress(h, m, t, bl, fl, rounds=7) + return h, m, t & MASK32, (t >> 32) & MASK32, bl, fl, out + + +def main(): + full = "--full" in sys.argv + print("=" * 70) + print("BLAKE3 compression-chip z3 gate") + print("=" * 70) + + # --- MAIN CHECK 0: single G (fundamental unit; covers every G/round) -- + print("\n=== MAIN CHECK 0: one G-function, free inputs (covers every G) ===") + g = check_g() + print(f" G (clean) -> {g} (want unsat)") + g_ok = (g == unsat) + + # --- MAIN CHECK 1: init-state layout + feed-forward (rounds=0) -------- + # Tiny & symbolic: v = initial state, then the feed-forward XORs. Isolates + # the h/IV/counter-split placement and out[i]=v[i]^v[i+8], out[i+8]=v[i+8]^h[i]. + print("\n=== MAIN CHECK 1: init-state + feed-forward (rounds=0, symbolic) ===") + r0 = check_compress(0) + print(f" compress rounds=0 -> {r0} (want unsat)") + wrapper_ok = (r0 == unsat) + + # --- Heavy symbolic multi-round UNSATs: BONUS, gated behind --full ---- + round_ok = None + full6 = full7 = full2 = None + if full: + print("\n=== MAIN CHECK 2 (--full): one round, free state+message ===") + rr = check_round(timeout_ms=1_800_000) + print(f" round (clean) -> {rr} (want unsat)") + round_ok = (rr == unsat) + print("\n=== MAIN CHECK 3 (--full): compression rounds=2 (permutation+chaining) ===") + full2 = check_compress(2, timeout_ms=1_800_000) + print(f" compress rounds=2 -> {full2} (want unsat)") + print("\n=== MAIN CHECK 4 (--full): FULL compression rounds=6 and rounds=7 ===") + full6 = check_compress(6, timeout_ms=2_400_000) + print(f" compress rounds=6 -> {full6} (want unsat)") + full7 = check_compress(7, timeout_ms=2_400_000) + print(f" compress rounds=7 -> {full7} (want unsat)") + else: + print("\n=== Heavy symbolic multi-round UNSATs skipped (pass --full) ===") + print(" G-unsat + fixed G-composition (chaining) already prove every round;") + print(" rounds=0 proves init+feed-forward; the message permutation is") + print(" proven load-bearing by the wrong_msg_index control and exercised") + print(" concretely by the full 6-/7-round positive controls below.") + + # --- NEGATIVE CONTROLS (must all be SAT) ----------------------------- + print("\n=== NEGATIVE CONTROLS — STRUCTURAL bugs (BV-observable, must be SAT) ===") + # NB: 'dropped carry booleanity' is deliberately NOT here. Dropping a carry + # column's booleanity is a FIELD-level soundness bug: an unconstrained + # committed column is a full field element, but in a *bounded BV* model the + # 8-bit carry + the s in [0,2^32) byte-range still pins s, so BV reports + # UNSAT. It is demonstrated correctly in the WIDTH AUDIT below (drop -> SAT), + # exactly as ../keccak-verify/hwsl_inline_test.py Part 2 requires the prime + # field to show HWSL bound-necessity. This is a feature: the gate separates + # BV-observable logic bugs from field-only soundness bugs. + controls = {} + controls["rot_wrong_amount"] = check_g(bug="rot_wrong_amount") # wrong rotation amount + controls["swap_g_operand"] = check_g(bug="swap_g_operand") # swapped G operand + controls["wrong_iv"] = check_compress(1, bug="wrong_iv") # wrong IV constant + controls["drop_ff_xor"] = check_compress(1, bug="drop_ff_xor") # dropped feed-forward XOR + controls["wrong_msg_index"] = check_compress(2, bug="wrong_msg_index") # wrong msg-schedule index + for name, res in controls.items(): + print(f" bug={name:18s} -> {res} (want sat)") + controls_ok = all(res == sat for res in controls.values()) + + # --- POSITIVE CONTROLS (external anchor: pin to oracle vectors) ------- + print("\n=== POSITIVE CONTROLS (pin input+output to oracle vectors -> SAT) ===") + vecs = load_canonical_6round() + pos_ok = True + for vec in vecs[:3]: + res = positive_control_compress( + 6, vec["h"], vec["m"], vec["t"] & MASK32, (vec["t"] >> 32) & MASK32, + vec["block_len"], vec["flags"], vec["out"]) + ok = (res == sat) + pos_ok &= ok + print(f" 6round seed={vec['seed']} (canonical) -> {res} (want sat)") + h7, m7, tlo7, thi7, bl7, fl7, out7 = gen_7round_vector() + res7 = positive_control_compress(7, h7, m7, tlo7, thi7, bl7, fl7, out7) + pos_ok &= (res7 == sat) + print(f" 7round (oracle-generated) -> {res7} (want sat)") + + # --- WIDTH AUDIT (field-level bound-necessity) ----------------------- + # These are the FIELD-level negative controls (BV provably cannot show them, + # since 2^16 / 2^32 are zero divisors mod 2^n). 'DROP -> sat' == the bug is + # exploitable in the prime field; 'present -> unsat' == the range check pins + # the value. Includes the 'dropped carry booleanity' control (team-lead #4). + print("\n=== WIDTH AUDIT + FIELD-LEVEL NEGATIVE CONTROLS (mod p bound necessity) ===") + a_sh = field_shift_bound(9, 0x9C3A, drop_sll_bound=False) + b_sh = field_shift_bound(9, 0x9C3A, drop_sll_bound=True) + print(f" shift r=9 AreBytes SLL bound present -> {a_sh} (want unsat: pinned)") + print(f" shift r=9 DROP SLL bound (neg ctrl) -> {b_sh} (want sat: forgeable)") + a_ad = field_add_carry(0xF0000000, 0xF0000000, 0xF0000000, drop_bool=False) + b_ad = field_add_carry(0xF0000000, 0xF0000000, 0xF0000000, drop_bool=True) + print(f" 3-add carry booleanity present -> {a_ad} (want unsat: pinned)") + print(f" 3-add DROP booleanity (neg ctrl #4) -> {b_ad} (want sat: forgeable)") + audit_ok = (a_sh == "unsat" and b_sh == "sat" and a_ad == "unsat" and b_ad == "sat") + + # --- VERDICT ---------------------------------------------------------- + print("\n" + "=" * 70) + print("VERDICT") + print("=" * 70) + print(f" G-function UNSAT (covers all G) : {g_ok}") + print(f" init+feed-forward UNSAT (rounds=0): {wrapper_ok}") + if full: + print(f" round UNSAT (direct) : {round_ok}") + print(f" compress rounds=2 UNSAT : {full2 == unsat}") + print(f" full 6-round UNSAT : {full6 == unsat}") + print(f" full 7-round UNSAT : {full7 == unsat}") + print(f" negative controls all SAT : {controls_ok}") + print(f" positive controls all SAT : {pos_ok} (full 6-/7-round pipeline, concrete)") + print(f" width audit (bound necessity) : {audit_ok}") + # G correctness + fixed G-composition => round correctness (chaining); + # rounds=0 => init+feed-forward; positive controls run the full pipeline + # concretely; the direct multi-round UNSATs (--full) are bonus confirmation. + base_ok = g_ok and wrapper_ok and controls_ok and pos_ok and audit_ok + full_ok = (not full) or (round_ok and full2 == unsat + and full6 == unsat and full7 == unsat) + ok = base_ok and full_ok + print(f"\n OVERALL: {'PASS' if ok else 'FAIL — investigate above'}") + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/thoughts/blake3/blake3-oracle/ORACLE.md b/thoughts/blake3/blake3-oracle/ORACLE.md new file mode 100644 index 000000000..aee71d1f0 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/ORACLE.md @@ -0,0 +1,368 @@ +# BLAKE3 Compression-Function Oracle + +**Purpose.** Trust anchor for a future BLAKE3 accelerator (precompile chip) in +the Lambda VM STARK prover. Phase 1 = this oracle (the reference `f` + external +validation + chip-contract reuse map). Phase 2 = chip constraint design, gated +against this oracle. The oracle is the reference the chip's trace generation and +constraints will be checked against; a wrong oracle silently poisons everything +downstream, so the validation section is the load-bearing part. + +**Scope.** The reference is the BLAKE3 **compression function** `f`, NOT the full +tree hash. `blake3_ref.py` also contains a full tree hasher, but that exists +*only* so `f` can be validated against the official whole-hash test vectors. The +chip implements `f`; it does not implement the tree. + +--- + +## 1. Validation status: **VALIDATED** + +`test_oracle.py` passes all of the following (re-run: `./venv/bin/python test_oracle.py`): + +| # | External anchor | Independent of our code? | What it covers | Result | +|---|---|---|---|---| +| 1 | Official **`test_vectors.json`** (BLAKE3 team, `test_vectors/test_vectors.json`, fetched from the BLAKE3 GitHub repo) | Yes — authored by the BLAKE3 authors | 35 input lengths (0 … 102400 B) × 3 modes (default hash, keyed hash, derive-key), extended (131-byte) output | **PASS 35/35 × 3** | +| 2 | Official **`blake3` PyPI package** v1.0.9 (the reference Rust implementation via FFI) | Yes — separate codebase | 23 randomised input lengths (0 … 100000 B) × {default, XOF, keyed, derive-key} = 92 differential checks | **PASS 92/92** | +| 3 | **Plonky3 `blake3-air`** compression, ported in `test_oracle.py` from `others/Plonky3/blake3-air/src/generation.rs` | Yes — Plonky3 team, different codebase | 20 000 random `(h, m, t, block_len)` compared at the **compression-function level** (flags = 0, 7 rounds) | **PASS 20000/20000** | + +Anchors 1–2 validate `f` *indirectly but exhaustively*: the whole-hash path +drives `f` under every flag combination (`CHUNK_START`, `CHUNK_END`, `PARENT`, +`ROOT`, `KEYED_HASH`, `DERIVE_KEY_CONTEXT`, `DERIVE_KEY_MATERIAL` and their +compositions) and a wide range of counters (chunk indices 0…99 for the 102400 B +case, plus XOF output-block counters). Anchor 3 validates `f` **directly** at the +compression level against a second independent implementation (flags = 0 only, +since Plonky3's AIR hardcodes `v[15] = 0`). + +The constants were independently cross-checked: `IV` and `MSG_PERMUTATION` in +`blake3_ref.py` match `others/Plonky3/blake3-air/src/constants.rs` (`IV` stored +there as `[lo16, hi16]` pairs; `MSG_PERMUTATION = [2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]`). + +> Note: the BLAKE3 repo's `reference_impl/reference_impl.py` returned HTTP 404 at +> fetch time (repo layout changed), so it is **not** used. `f` was written from +> the spec's G-function definition; the three anchors above stand on their own. + +--- + +## 2. Precise definition of both variants + +Everything is on 32-bit unsigned words, little-endian. `⊞` = add mod 2³², +`⊕` = XOR, `x ⋙ n` = rotate-right by `n` bits. + +### 2.1 Constants + +``` +IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] + +MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +``` + +### 2.2 The G function (quarter round) + +`G(v, a, b, c, d, mx, my)` mutates working-state words `v[a], v[b], v[c], v[d]`: + +``` +v[a] = v[a] ⊞ v[b] ⊞ mx +v[d] = (v[d] ⊕ v[a]) ⋙ 16 +v[c] = v[c] ⊞ v[d] +v[b] = (v[b] ⊕ v[c]) ⋙ 12 +v[a] = v[a] ⊞ v[b] ⊞ my +v[d] = (v[d] ⊕ v[a]) ⋙ 8 +v[c] = v[c] ⊞ v[d] +v[b] = (v[b] ⊕ v[c]) ⋙ 7 +``` + +### 2.3 The round + +Given the (already permuted-for-this-round) 16-word schedule `m`: + +``` +# columns +G(v, 0, 4, 8, 12, m[0], m[1]) +G(v, 1, 5, 9, 13, m[2], m[3]) +G(v, 2, 6, 10, 14, m[4], m[5]) +G(v, 3, 7, 11, 15, m[6], m[7]) +# diagonals +G(v, 0, 5, 10, 15, m[8], m[9]) +G(v, 1, 6, 11, 12, m[10], m[11]) +G(v, 2, 7, 8, 13, m[12], m[13]) +G(v, 3, 4, 9, 14, m[14], m[15]) +``` + +### 2.4 The compression function `f` (parameterised by `ROUNDS`) + +Inputs: `h[0..8]` (chaining value, 8×u32), `m[0..16]` (message block, 16×u32), +`t` (u64 counter), `block_len` (u32, 0..64), `flags` (u32). + +``` +v[0..8] = h[0..8] +v[8..12] = IV[0..4] +v[12] = t mod 2³² # counter low +v[13] = t >> 32 # counter high +v[14] = block_len +v[15] = flags + +schedule = m +for r in 0 .. ROUNDS-1: + round(v, schedule) + schedule = permute(schedule) # trailing permute after last round is unused + +# feed-forward (produces the FULL 16-word output) +for i in 0..8: + out[i] = v[i] ⊕ v[i+8] + out[i+8] = v[i+8] ⊕ h[i] +return out[0..16] +``` + +The truncated 8-word chaining value used inside the tree is `out[0:8]`. The XOF +root output uses **all 16** output words — this is why `f` returns 16 words. + +### 2.5 Variant A — standard: `ROUNDS = 7` + +The function above with `ROUNDS = 7`. This is standard BLAKE3, validated by +anchors 1–3. + +### 2.6 Variant B — nonstandard: `ROUNDS = 6` + +**Exactly** the function in §2.4 with `ROUNDS = 6`: rounds 0..5 are applied, +round `r` mixing `permute^r(m)`, followed by the identical feed-forward. The +ONLY difference from Variant A is the loop bound. This is a **NONSTANDARD** +function; **no external test vectors exist**. Its anchoring is derivative: + +* **(a) Code-diff anchor.** In `blake3_ref.py`, `compress_6round(...)` is literally + `compress(..., rounds=6)` — same IV, same initial-state layout, same G, same + message permutation schedule, same feed-forward. `test_oracle.py` + (`test_6round_derivation`) asserts `compress_6round == compress(rounds=6)` and + that it differs from `ROUNDS=7` on 2000/2000 random inputs. +* **(b) Canonical vectors.** 10 deterministic vectors (fixed seeds 0..9) are + generated and recorded below. These are Variant B's canonical reference going + forward. Full inputs/outputs are in `canonical_6round_vectors.json`. + +#### Canonical 6-round vectors (seeds 0..9) + +Each row: 32-hex-digit words. `out` is the full 16-word output concatenated +(`out[0]` first). Inputs `h` (8 words), `m` (16 words), `t`, `block_len`, +`flags` are in `canonical_6round_vectors.json`; a summary fingerprint is shown +here (`out[0]` and `out[15]`) so the doc alone pins the vectors' identity. + +| seed | t | block_len | flags | out[0] | out[15] | +|---|---|---|---|---|---| +| 0 | 0xb4e1357d4a84eb03 | 42 | 0x34 | 0xced9d1ff | 0xb75f3915 | +| 1 | 0xc74803e31ba16215 | 50 | 0x5e | 0xf2a972e9 | 0xdfb91125 | +| 2 | 0x7604e4b4e73695c3 | 58 | 0x7c | 0x5aa6b114 | 0x775f2f92 | +| 3 | 0x92d3043afcf249f3 | 36 | 0x1f | 0xeed92fab | 0xdc293166 | +| 4 | 0x49c7b59b995253fd | 57 | 0x29 | 0xca00bda3 | 0x7561eb37 | +| 5 | 0x6a3753915c76f18a | 18 | 0x43 | 0x14a9f66f | 0xbb7a485d | +| 6 | 0x390567c27bd6aa42 | 26 | 0x03 | 0x32a6ff70 | 0x2a7a62b2 | +| 7 | 0x12bd4acefaecbd38 | 53 | 0x2a | 0xa632ad45 | 0xf3f33689 | +| 8 | 0x329911da9fbd8735 | 19 | 0x5b | 0x913b2ae1 | 0x3c5a654b | +| 9 | 0xeaeb999b8a2e547e | 64 | 0x15 | 0xf5ee9114 | 0xd18a8b94 | + +(To re-derive: `random.Random(seed)` then draw `h=8×u32, m=16×u32, t=u64, +block_len∈[0,65), flags∈[0,128)` in that order — see +`test_oracle.canonical_6round_vectors`.) + +--- + +## 3. Chip-contract reuse map + +Every primitive op of `f` mapped onto the existing precomputed-table contracts. +Citations are to `prover/src/tables/bitwise.rs` (the 2²⁰-row BITWISE table) and +the KECCAK chips, which are the architectural template for a byte-oriented +delegation chip. + +The BITWISE table (`bitwise.rs:97`, `NUM_ROWS = 256·256·16 = 2²⁰`) is indexed by +`(X: byte, Y: byte, Z: 4-bit)` and provides these receivers +(`bitwise.rs:715` `bus_interactions`): + +* `ByteAlu[opsel, X, Y] → out` — byte AND/OR/XOR (`bitwise.rs:865-921`; `opsel` + ∈ {AND, OR, XOR}). The output column is a table column, so a `ByteAlu` send + **simultaneously range-checks X and Y to be bytes and pins `out` to the exact + result** — no separate range check needed on any of the three. +* `ARE_BYTES[X, Y]` — range-check two bytes (`bitwise.rs:783`; pass `Y=0` for a + single byte). +* `IS_HALF[X + 256·Y]` — range-check a 16-bit halfword (`bitwise.rs:798`). +* `IS_B20[...]` — 20-bit range check (`bitwise.rs:813`). +* `HWSL[X + 256·Y, Z] → [SLL, SLLC]` — halfword shift-left (`bitwise.rs:831`), + where `SLL = (hw << Z) & 0xFFFF`, `SLLC = hw >> (16 - Z)` (`bitwise.rs:135-141`), + `Z ∈ [0,16)`. +* `MSB8`, `MSB16`, `ZERO` — not needed by BLAKE3. + +### 3.1 Op-by-op mapping + +| BLAKE3 primitive | Existing contract | How | Cost | +|---|---|---|---| +| **32-bit XOR** (`v[d]⊕v[a]`, `v[b]⊕v[c]`, feed-forward) | `ByteAlu[XOR]` | 4 byte-XOR lookups per 32-bit word, one per byte, exactly as `keccak_rnd` does θ/χ/ι XORs (`keccak_rnd.rs:692-718`). Inputs & output auto-range-checked by the lookup. | 4 sends / 32-bit XOR | +| **`⋙ 16`** | *free* — byte relabeling | rotr16 permutes bytes `[b0,b1,b2,b3] → [b2,b3,b0,b1]`. **VERIFIED** exhaustively (100k random words). No lookup, no column: just re-address the bytes at the next use. | 0 | +| **`⋙ 8`** | *free* — byte relabeling | rotr8 → `[b1,b2,b3,b0]`. **VERIFIED**. | 0 | +| **`⋙ 12`** | `HWSL` (+ `ARE_BYTES`) | rotr12 = rotl20; per the keccak-ρ pattern, HWSL each of the 2 halfwords by `rnc=4`, then a halfword rotate by `rbc=1`, recombining `newlo = SLL_lo + SLLC_hi`, `newhi = SLL_hi + SLLC_lo` (non-overlapping bit ranges ⇒ add = OR), then swap the two halfwords. **VERIFIED** (50k random). Range-check the 4 output bytes with `ARE_BYTES` (as keccak does on ρ outputs, `keccak_rnd.rs:768-790`). | 2 HWSL + 2 ARE_BYTES / rot | +| **`⋙ 7`** | `HWSL` (+ `ARE_BYTES`) | rotr7 = rotl25; same pattern with `rnc=9`, `rbc=1`. **VERIFIED**. `rnc=9 < 16` fits HWSL's 4-bit `Z`. | 2 HWSL + 2 ARE_BYTES / rot | +| **32-bit add mod 2³²** (2-operand `v[c]⊞v[d]`) | carry-bit polynomial constraint + range-check | Exactly `emit_add_pair`'s low half (`templates.rs:334`): with sum `s` committed and range-checked, `carry = (a + b − s)·2⁻³²` is constrained `carry·(1−carry)=0` (`INV_SHIFT_32 = (2³²)⁻¹`, `templates.rs:26`). Sum bytes are range-checked *for free* because `s` immediately feeds an XOR lookup. | 1 poly constraint / add | +| **3-operand add mod 2³²** (`v[a]⊞v[b]⊞mx`) | carry constraint (see §5 open Q) | `a+b+m < 3·2³²` ⇒ carry ∈ {0,1,2}. Either one virtual `k(k−1)(k−2)=0` (deg 3) or two chained `emit_add_pair` steps (deg ≤ 2). See open question O1. | 1–2 poly constraints / add | +| **message schedule** (`permute` between rounds) | *free* — wiring | Fixed compile-time permutation of the 16 input words per round; round `r` references `permute^r`-indexed message columns. No table, exactly like `keccak_rnd` inlines `KECCAK_RHO` offsets as compile-time constants. **Confirmed.** | 0 | +| **IV constants, flags, block_len, counter split** | constants / direct columns | `IV[0..4] → v[8..12]`, `t` split into `v[12]=t mod 2³²`, `v[13]=t>>32`, `v[14]=block_len`, `v[15]=flags`. Constants inlined; counter split is two committed words range-checked. | ~0 | + +**No BLAKE3 op lacks an existing contract.** All arithmetic reduces to +`ByteAlu[XOR]`, `HWSL`, `ARE_BYTES`, and the `emit_add_pair` carry template — +every one already exercised by the KECCAK chips. The 32-bit-add carry range +checks fit `ARE_BYTES`/`IS_HALF` (the sum's bytes/halfwords), and the carry +itself is a `{0,1}` (or `{0,1,2}`) polynomial bit, not a table lookup. + +### 3.2 Why the two "free" rotations are actually free + +`ByteAlu` and `HWSL` operate at byte / halfword granularity, and the working +state is stored as bytes. A rotate-right by a multiple of 8 is a permutation of +byte positions, so the constraint at the *consuming* site simply reads the bytes +in rotated order (the same trick keccak uses implicitly). Only `⋙12` and `⋙7` +cross byte boundaries and therefore need HWSL. This means **half** of BLAKE3's +rotations cost nothing. + +--- + +## 4. I/O column boundary sketch + +Analogous to keccak's 200-byte state handoff (`keccak.rs`), the chip's +bus-facing tuple. Recommended **granularity: bytes** — because XOR (the dominant +op) needs byte operands and the two byte-aligned rotations are free at byte +granularity; adds read bytes as a linear combination (`AddOperand::from_dword_bl`, +`templates.rs:191`) so byte storage costs them nothing. + +**Chip input** (read from guest memory via the ECALL/MEMW interface, exactly the +keccak pattern `keccak.rs:160-449`: ECALL receiver binds the syscall + timestamp, +a MEMW read of `x10` binds the state pointer, then per-word MEMW reads): + +| field | size | granularity | +|---|---|---| +| `h[0..8]` chaining value | 8 words = 32 B | bytes | +| `m[0..16]` message block | 16 words = 64 B | bytes | +| `t` counter | u64 = 8 B | 2 words (lo, hi), byte-stored | +| `block_len` | u32 | 1 word | +| `flags` | u32 | 1 word | + +**Chip output** (written back to memory): + +| field | size | granularity | +|---|---|---| +| `out[0..16]` | 16 words = 64 B | bytes | + +For the truncated (CV-only) call sites the guest reads back `out[0:8]`; the chip +always produces the full 16 words (the XOF root needs them). + +**Internal handoff (if one-row-per-round).** If the chip mirrors keccak's +round-chip split, a `Blake3Round` bus carries `(timestamp, round_index, +state[16 words as 64 bytes], message[16 words])` from row `r` to row `r+1`, +mirroring `keccak_rnd`'s `(timestamp, round, start[200])` handoff +(`keccak_rnd.rs:441-515`). Note BLAKE3 must also carry the (round-permuted) +message down the rounds, unlike keccak whose round chip has no message input. + +--- + +## 5. Cost estimate & recommended granularity + +Cost model (given): a **committed** cell is expensive; each **bus send** ≈ 1.5 +base cells of aux; **max constraint degree 3** is a hard cap. + +### Per-round work (8 G calls; each G = 2 three-operand adds, 2 two-operand adds, +4 XORs, 4 rotations of which 2 are free): + +| resource | per round | note | +|---|---|---| +| `ByteAlu[XOR]` sends | 8·4·4 = **128** | 4 XORs/G × 4 bytes | +| `HWSL` sends | 8·2·2 = **32** | 2 non-free rots/G × 2 halfwords | +| `ARE_BYTES` (rot-output range checks) | ~**32** | 2 rots/G × 4 bytes ÷ 2-per-send | +| add carry constraints | ~**48** | (16 three-op + 16 two-op adds)/round | +| committed byte-cells (state + G intermediates + carries) | ~**450** | ~10 words/G committed × 8 G × 4 B + input state | + +Bus sends/round ≈ 128 + 32 + 32 ≈ **~190**; aux ≈ 190 × 1.5 ≈ **~290** base +cells; committed ≈ **~450**. Total ≈ **~740 cell-equivalents/round**. + +### Per compression (7 rounds + feed-forward + I/O): + +* XOR lookups: 7·128 + 64 (feed-forward) ≈ **~960** +* HWSL lookups: 7·32 ≈ **~224** +* Range-check sends: ~7·32 + I/O ≈ **~250** +* **Total bus sends ≈ ~1450**, aux ≈ ~2200 base cells +* Committed ≈ 7·450 + I/O ≈ **~3300** base cells +* **Grand total ≈ ~5000–6000 cell-equivalents per compression**, dominated by + the ~960 byte-XOR lookups. + +For scale: a keccak-f permutation is ~24 rounds × 1480 cols. A BLAKE3 +compression is roughly **¼–⅓ of one keccak permutation**. + +### Recommended layout + +BLAKE3 has only **7 rounds** (vs keccak's 24). Two viable shapes: + +* **A. One row per round** (keccak-style): ~450–750 columns/row × 7 rows, plus a + `Blake3Round` internal handoff bus carrying state **and** the permuted message. + Fewer columns, but the message-carrying handoff is extra bus traffic keccak + doesn't have. +* **B. One row per compression** (fully unrolled): ~3000–3500 columns in a single + row; no internal handoff bus, no round-index bookkeeping. The message schedule + is pure compile-time wiring so unrolling is natural. + +**Recommendation: start with B (one row per compression).** With only 7 rounds +the column count (~3k) is comparable to keccak's per-round width, and eliminating +the internal state+message handoff bus removes the biggest source of aux cost and +constraint complexity. Revisit A only if the committed width dominates trace-area +budget. Either way the cell total is the same order (~5–6k). + +--- + +## 6. Open questions for the chip phase + +* **O1 — 3-operand add carry granularity (the main one).** `v[a] = v[a] ⊞ v[b] ⊞ + mx` sums three 32-bit values, so the carry-out is in **{0,1,2}**, not {0,1}. + `emit_add_pair` (`templates.rs:334`) only handles a `{0,1}` carry. Options: + 1. **One virtual carry ∈ {0,1,2}:** commit the sum `s` (range-checked), + `k = (a+b+m−s)·2⁻³²`, constrain `k(k−1)(k−2)=0`. This is **degree 3** — at + the cap. It cannot also be `μ`-gated (that would be degree 4). Feasible only + if padding rows satisfy it ungated (all-zero padding ⇒ `k=0` ⇒ satisfied, + the keccak padding convention — verify this holds for BLAKE3 padding). + 2. **Two chained adds:** `t = a ⊞ b` (carry ∈ {0,1}), then `a' = t ⊞ mx` (carry + ∈ {0,1}), each via `emit_add_pair`, at the cost of one extra committed 32-bit + intermediate `t` per 3-operand add (16 extra words/round). Stays degree ≤ 2, + so it can be `μ`-gated to degree 3. Simpler and gate-friendly. + * **Recommendation:** option 2 (chained adds) unless the extra committed width + is measured to hurt — it keeps every add uniformly `{0,1}`-carry and leaves + degree headroom for `μ`-gating. Decide with a bench once the chip exists. + +* **O2 — carry-bit gating & padding.** Decide whether add-carry and rot + constraints are `μ`-gated (like `keccak_rnd`'s IS_BIT, `keccak_rnd.rs:914`) or + rely on all-zero padding rows satisfying them ungated. This interacts with O1's + degree budget. + +* **O3 — one-row-per-round vs unrolled (§5).** Ties to O2 and to whether the + message schedule is carried on a handoff bus or wired per-row at compile time. + +* **O4 — flags coverage of the direct anchor.** Anchor 3 (Plonky3) only checks + `flags = 0` at the compression level; non-zero flags are validated only through + the whole-hash anchors 1–2. If the chip is ever exercised on raw compression + inputs with arbitrary flags outside a valid tree, add a direct differential + check against the PyPI package's low-level API if/when it exposes `compress` + (it currently does not). + +* **O5 — counter (`t`) width.** The whole-hash anchors drive `t` only up to ~99 + (chunk index) plus small XOF counters. The chip must accept a full u64 `t` + (`v[12]/v[13]` split). Constants and the split are validated structurally, but + if the chip supports enormous counters, add a targeted vector. (Plonky3 anchor + already exercises random full-width u64 `t` — so this is **covered**.) + +* **O6 — endianness at the memory boundary.** BLAKE3 words are little-endian; + the byte-granular I/O sketch (§4) assumes LE byte order in memory. Confirm + against the guest's `blake3` calling convention when wiring MEMW. + +--- + +## 7. File manifest + +``` +blake3-oracle/ +├── blake3_ref.py # reference f (ROUNDS-parameterised) + 6-round variant + tree hasher +├── test_oracle.py # anchors 1-3 + 6-round derivation + canonical-vector emitter +├── ORACLE.md # this document +├── official_test_vectors.json # BLAKE3 team vectors (fetched, unmodified) +├── canonical_6round_vectors.json # 10 canonical Variant-B vectors (generated) +└── venv/ # python venv with the official `blake3` pkg (anchor 2) +``` + +No repository files were modified. diff --git a/thoughts/blake3/blake3-oracle/blake3_ref.py b/thoughts/blake3/blake3-oracle/blake3_ref.py new file mode 100644 index 000000000..ee2d48e85 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/blake3_ref.py @@ -0,0 +1,399 @@ +""" +BLAKE3 compression-function ORACLE (reference implementation). + +This is the TRUST ANCHOR for a future BLAKE3 accelerator chip in the Lambda VM +STARK prover. It is written directly from the BLAKE3 specification / reference +design, NOT copied from any implementation, and then validated externally in +`test_oracle.py` against: + - the official BLAKE3 team's `test_vectors.json`, + - the official `blake3` PyPI package (the reference Rust implementation), + - Plonky3's independent `blake3-air` compression implementation. + +Spec sources used while writing this file (all public): + - BLAKE3 paper / spec, section 2.1-2.2 (compression function, G, round). + - The reference message-permutation schedule and IV constants, which also + appear verbatim in the vendored Plonky3 `blake3-air/src/constants.rs` + (IV, MSG_PERMUTATION) — used here only as a cross-check of the constants, + the mixing logic is written from the spec's G-function definition. + +Everything operates on 32-bit unsigned words, little-endian, exactly as BLAKE3 +specifies. +""" + +# --------------------------------------------------------------------------- +# Constants (BLAKE3 spec, section 2.1) +# --------------------------------------------------------------------------- + +# Initialisation vector: the first 8 words of the SHA-256 IV (fractional parts +# of the square roots of the first 8 primes). Identical to SHA-256 / BLAKE2s. +IV = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +] + +# Message word permutation applied between successive rounds. After each round +# the 16 message words are permuted by this index map; round r therefore mixes +# the original message under permutation^r. (BLAKE3 spec / reference schedule.) +MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] + +# Domain-separation flags (BLAKE3 spec, table of flags). +CHUNK_START = 1 << 0 # 0x01 +CHUNK_END = 1 << 1 # 0x02 +PARENT = 1 << 2 # 0x04 +ROOT = 1 << 3 # 0x08 +KEYED_HASH = 1 << 4 # 0x10 +DERIVE_KEY_CONTEXT = 1 << 5 # 0x20 +DERIVE_KEY_MATERIAL = 1 << 6 # 0x40 + +# Structural sizes. +BLOCK_LEN = 64 # bytes per compression input block (16 words * 4 bytes) +CHUNK_LEN = 1024 # bytes per chunk (16 blocks) +KEY_LEN = 32 # bytes in a key / chaining value (8 words * 4 bytes) +OUT_LEN = 32 # default output length in bytes + +MASK32 = 0xFFFFFFFF + +# Standard round count for BLAKE3. Variant B is the same function with ROUNDS=6. +DEFAULT_ROUNDS = 7 + + +# --------------------------------------------------------------------------- +# 32-bit word primitives (BLAKE3 spec, section 2.1 "G function") +# --------------------------------------------------------------------------- + +def add32(a, b): + """Addition modulo 2^32 (wrapping).""" + return (a + b) & MASK32 + + +def rotr(x, n): + """Rotate the 32-bit word `x` RIGHT by `n` bits. + + BLAKE3's G uses rotation amounts 16, 12, 8, 7. Rotations by 16 and 8 are + byte-aligned (multiples of 8); 12 and 7 are not. The chip-contract reuse + map in ORACLE.md analyses each of these against the HWSL lookup table. + """ + x &= MASK32 + return ((x >> n) | (x << (32 - n))) & MASK32 + + +def g(state, a, b, c, d, mx, my): + """The BLAKE3 quarter-round mixing function G (spec section 2.1). + + Mixes two message words `mx`, `my` into four state words at indices + a, b, c, d of the 16-word working state. Two "half rounds" of the form + add / xor+rotate: + + v[a] = v[a] + v[b] + mx + v[d] = (v[d] ^ v[a]) >>> 16 + v[c] = v[c] + v[d] + v[b] = (v[b] ^ v[c]) >>> 12 + v[a] = v[a] + v[b] + my + v[d] = (v[d] ^ v[a]) >>> 8 + v[c] = v[c] + v[d] + v[b] = (v[b] ^ v[c]) >>> 7 + """ + state[a] = add32(add32(state[a], state[b]), mx) + state[d] = rotr(state[d] ^ state[a], 16) + state[c] = add32(state[c], state[d]) + state[b] = rotr(state[b] ^ state[c], 12) + state[a] = add32(add32(state[a], state[b]), my) + state[d] = rotr(state[d] ^ state[a], 8) + state[c] = add32(state[c], state[d]) + state[b] = rotr(state[b] ^ state[c], 7) + + +def round_fn(state, m): + """One BLAKE3 round: 4 column mixes then 4 diagonal mixes (spec 2.1). + + `m` is the (already-permuted for this round) 16-word message schedule. + The G calls consume message words m[0..16] in order. + """ + # Mix the columns. + g(state, 0, 4, 8, 12, m[0], m[1]) + g(state, 1, 5, 9, 13, m[2], m[3]) + g(state, 2, 6, 10, 14, m[4], m[5]) + g(state, 3, 7, 11, 15, m[6], m[7]) + # Mix the diagonals. + g(state, 0, 5, 10, 15, m[8], m[9]) + g(state, 1, 6, 11, 12, m[10], m[11]) + g(state, 2, 7, 8, 13, m[12], m[13]) + g(state, 3, 4, 9, 14, m[14], m[15]) + + +def permute(m): + """Apply MSG_PERMUTATION to a 16-word message list, returning a new list.""" + return [m[MSG_PERMUTATION[i]] for i in range(16)] + + +# --------------------------------------------------------------------------- +# The compression function `f` (BLAKE3 spec, section 2.2) +# --------------------------------------------------------------------------- + +def compress(chaining_value, block_words, counter, block_len, flags, + rounds=DEFAULT_ROUNDS): + """BLAKE3 compression function. + + Inputs: + chaining_value : list of 8 u32 words (h[0..8]) + block_words : list of 16 u32 words (m[0..16]) + counter : u64 block counter t + block_len : u32 number of input bytes in this block (0..64) + flags : u32 domain-separation flags + rounds : number of rounds (7 = standard, 6 = variant B) + + Returns a list of 16 u32 words: the full compression output. The truncated + 8-word chaining value used elsewhere in the tree is `output[0:8]`. + + The 16-word initial working state v is: + v[0..8] = chaining_value[0..8] + v[8..12] = IV[0..4] + v[12] = counter mod 2^32 (low 32 bits of t) + v[13] = counter >> 32 (high 32 bits of t) + v[14] = block_len + v[15] = flags + Then `rounds` rounds are applied, permuting the message schedule between + rounds. Finally the feed-forward XOR produces the 16-word output: + output[i] = v[i] ^ v[i+8] for i in 0..8 + output[i+8] = v[i+8] ^ chaining_value[i] for i in 0..8 + """ + assert len(chaining_value) == 8 + assert len(block_words) == 16 + assert 0 <= counter < (1 << 64) + + counter_low = counter & MASK32 + counter_high = (counter >> 32) & MASK32 + + state = [ + chaining_value[0], chaining_value[1], chaining_value[2], chaining_value[3], + chaining_value[4], chaining_value[5], chaining_value[6], chaining_value[7], + IV[0], IV[1], IV[2], IV[3], + counter_low & MASK32, counter_high & MASK32, block_len & MASK32, flags & MASK32, + ] + + # Local copy of the message schedule; permuted between rounds. + m = list(block_words) + for r in range(rounds): + round_fn(state, m) + # Permute between rounds. The permutation after the final round is + # never consumed, so applying it only for r < rounds-1 is equivalent; + # we permute between rounds to keep the loop structure obvious. + if r < rounds - 1: + m = permute(m) + + # Feed-forward XOR producing the full 16-word output. + output = [0] * 16 + for i in range(8): + output[i] = state[i] ^ state[i + 8] + output[i + 8] = state[i + 8] ^ chaining_value[i] + return output + + +def compress_cv(chaining_value, block_words, counter, block_len, flags, + rounds=DEFAULT_ROUNDS): + """The truncated 8-word chaining value: first 8 words of `compress`.""" + return compress(chaining_value, block_words, counter, block_len, flags, + rounds)[:8] + + +# =========================================================================== +# Variant B: 6-round BLAKE3 compression. +# +# This is EXACTLY `compress(..., rounds=6)`. It is a NONSTANDARD function with +# no external test vectors; ORACLE.md documents its canonical vectors. The only +# difference from the validated 7-round function is the loop bound `rounds`. +# =========================================================================== + +def compress_6round(chaining_value, block_words, counter, block_len, flags): + """6-round variant of the BLAKE3 compression function (variant B). + + Rounds 0..5 are applied with message permutations 0..5 (i.e. round r mixes + permute^r(block_words)), then the identical feed-forward XOR finalisation. + Everything else — IV, initial state layout, G function, feed-forward — is + bit-for-bit identical to the 7-round function. + """ + return compress(chaining_value, block_words, counter, block_len, flags, + rounds=6) + + +# =========================================================================== +# Full BLAKE3 tree hash, built ON TOP of `compress`. +# +# This exists ONLY so the compression function can be validated against the +# official whole-hash test vectors (which exercise `compress` under every flag +# combination and many counter values). The chip does NOT implement the tree; +# it implements `compress`. Written from the spec's tree/chunk structure. +# =========================================================================== + +def words_from_le_bytes(b): + """Convert a bytes object (len multiple of 4) into a list of u32 words.""" + assert len(b) % 4 == 0 + return [int.from_bytes(b[i:i + 4], "little") for i in range(0, len(b), 4)] + + +def le_bytes_from_words(words): + return b"".join((w & MASK32).to_bytes(4, "little") for w in words) + + +class _Output: + """A not-yet-finalised node (chunk or parent). Can emit a chaining value + or an extendable root output (spec section 2.3, XOF).""" + + def __init__(self, input_cv, block_words, counter, block_len, flags, rounds): + self.input_cv = input_cv + self.block_words = block_words + self.counter = counter + self.block_len = block_len + self.flags = flags + self.rounds = rounds + + def chaining_value(self): + return compress(self.input_cv, self.block_words, self.counter, + self.block_len, self.flags, self.rounds)[:8] + + def root_output_bytes(self, out_len): + out = bytearray() + counter = 0 + while len(out) < out_len: + words = compress(self.input_cv, self.block_words, counter, + self.block_len, self.flags | ROOT, self.rounds) + # The ROOT output uses ALL 16 output words (this is why compress + # returns 16 words rather than the truncated 8). + out += le_bytes_from_words(words) + counter += 1 + return bytes(out[:out_len]) + + +class _ChunkState: + def __init__(self, key_words, chunk_counter, flags, rounds): + self.cv = list(key_words) + self.chunk_counter = chunk_counter + self.block = b"" + self.blocks_compressed = 0 + self.flags = flags + self.rounds = rounds + + def _start_flag(self): + return CHUNK_START if self.blocks_compressed == 0 else 0 + + def update(self, data): + while data: + if len(self.block) == BLOCK_LEN: + block_words = words_from_le_bytes(self.block) + self.cv = compress(self.cv, block_words, self.chunk_counter, + BLOCK_LEN, self.flags | self._start_flag(), + self.rounds)[:8] + self.blocks_compressed += 1 + self.block = b"" + take = min(BLOCK_LEN - len(self.block), len(data)) + self.block += data[:take] + data = data[take:] + + def output(self): + block_words = words_from_le_bytes(self.block + b"\x00" * (BLOCK_LEN - len(self.block))) + return _Output(self.cv, block_words, self.chunk_counter, len(self.block), + self.flags | self._start_flag() | CHUNK_END, self.rounds) + + +def _parent_output(left_cv, right_cv, key_words, flags, rounds): + block_words = left_cv + right_cv # 16 words + return _Output(list(key_words), block_words, 0, BLOCK_LEN, flags | PARENT, rounds) + + +class Blake3Hasher: + """Minimal BLAKE3 tree hasher over the reference `compress`. + + Supports the three official modes (default hash, keyed hash, derive-key) + and extendable output, so it can be checked against `test_vectors.json`. + """ + + def __init__(self, key_words, flags, rounds=DEFAULT_ROUNDS): + self.key_words = list(key_words) + self.flags = flags + self.rounds = rounds + self.chunk_state = _ChunkState(self.key_words, 0, flags, rounds) + self.cv_stack = [] # list of 8-word chaining values + + @classmethod + def default(cls, rounds=DEFAULT_ROUNDS): + return cls(IV, 0, rounds) + + @classmethod + def keyed(cls, key32, rounds=DEFAULT_ROUNDS): + assert len(key32) == KEY_LEN + return cls(words_from_le_bytes(key32), KEYED_HASH, rounds) + + @classmethod + def derive_key(cls, context_string, rounds=DEFAULT_ROUNDS): + # Phase 1: hash the context string in DERIVE_KEY_CONTEXT mode to get a + # 32-byte context key; Phase 2: keyed-hash the material with that key + # under DERIVE_KEY_MATERIAL. + ctx_hasher = cls(IV, DERIVE_KEY_CONTEXT, rounds) + ctx_hasher.update(context_string.encode("utf-8") if isinstance(context_string, str) else context_string) + context_key = ctx_hasher.finalize(KEY_LEN) + return cls(words_from_le_bytes(context_key), DERIVE_KEY_MATERIAL, rounds) + + def _add_chunk_cv(self, new_cv, total_chunks): + # Merge the CV stack following the binary-tree structure. A completed + # subtree is merged whenever the total chunk count is even at that level. + while total_chunks & 1 == 0: + left = self.cv_stack.pop() + new_cv = _parent_output(left, new_cv, self.key_words, self.flags, + self.rounds).chaining_value() + total_chunks >>= 1 + self.cv_stack.append(new_cv) + + def update(self, data): + data = bytes(data) + while data: + if len(self.chunk_state.block) == BLOCK_LEN and \ + self.chunk_state.blocks_compressed == CHUNK_LEN // BLOCK_LEN - 1: + # current chunk is full: finalise it and start a new one. + chunk_cv = self.chunk_state.output().chaining_value() + total_chunks = self.chunk_state.chunk_counter + 1 + self._add_chunk_cv(chunk_cv, total_chunks) + self.chunk_state = _ChunkState(self.key_words, total_chunks, + self.flags, self.rounds) + # How many bytes still fit in the current chunk. + want = CHUNK_LEN - self._chunk_len() + take = min(want, len(data)) + self.chunk_state.update(data[:take]) + data = data[take:] + + def _chunk_len(self): + return self.chunk_state.blocks_compressed * BLOCK_LEN + len(self.chunk_state.block) + + def finalize(self, out_len=OUT_LEN): + # Walk the current chunk's output up the CV stack, XORing/parenting all + # the way to the root, and emit the root output. + output = self.chunk_state.output() + parent_nodes_remaining = len(self.cv_stack) + while parent_nodes_remaining > 0: + parent_nodes_remaining -= 1 + left = self.cv_stack[parent_nodes_remaining] + output = _parent_output(left, output.chaining_value(), + self.key_words, self.flags, self.rounds) + return output.root_output_bytes(out_len) + + +def blake3_hash(data, out_len=OUT_LEN, rounds=DEFAULT_ROUNDS): + h = Blake3Hasher.default(rounds) + h.update(data) + return h.finalize(out_len) + + +def blake3_keyed_hash(key32, data, out_len=OUT_LEN, rounds=DEFAULT_ROUNDS): + h = Blake3Hasher.keyed(key32, rounds) + h.update(data) + return h.finalize(out_len) + + +def blake3_derive_key(context_string, key_material, out_len=OUT_LEN, rounds=DEFAULT_ROUNDS): + h = Blake3Hasher.derive_key(context_string, rounds) + h.update(key_material) + return h.finalize(out_len) + + +if __name__ == "__main__": + # Tiny smoke test: empty-input default hash (compare to test_oracle.py). + print("blake3('') =", blake3_hash(b"").hex()) diff --git a/thoughts/blake3/blake3-oracle/test_oracle.py b/thoughts/blake3/blake3-oracle/test_oracle.py new file mode 100644 index 000000000..48a617bec --- /dev/null +++ b/thoughts/blake3/blake3-oracle/test_oracle.py @@ -0,0 +1,327 @@ +""" +Validation suite for the BLAKE3 compression-function oracle. + +External anchors (independent of `blake3_ref.py`): + 1. Official BLAKE3 `test_vectors.json` (authored by the BLAKE3 team). Covers + the whole-hash output in all three modes (hash / keyed_hash / derive_key) + for 35 input lengths up to 102400 bytes. Passing these exercises the + compression function under every flag combination and many counter values. + 2. The official `blake3` PyPI package (the reference Rust implementation), + differential-tested on randomised inputs of many lengths in all 3 modes. + 3. Plonky3's independent `blake3-air` compression (ported below from + others/Plonky3/blake3-air/src/generation.rs), differential-tested DIRECTLY + at the compression-function level (flags = 0) on random (h, m, t, block_len). + +The 6-round variant has no external vectors; we (a) show it differs from the +7-round function only in the round count and (b) emit 10 canonical vectors. + +Run: ./venv/bin/python test_oracle.py +""" + +import json +import os +import random +import sys + +import blake3_ref as ref + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# Test inputs in test_vectors.json follow a fixed pattern: byte i is (i % 251). +def pattern_input(n): + return bytes(i % 251 for i in range(n)) + + +# --------------------------------------------------------------------------- +# ANCHOR 1: official BLAKE3 test_vectors.json +# --------------------------------------------------------------------------- + +def test_official_vectors(): + path = os.path.join(HERE, "official_test_vectors.json") + data = json.load(open(path)) + key = data["key"].encode("utf-8") + assert len(key) == 32, f"expected 32-byte key, got {len(key)}" + context = data["context_string"] + + cases = data["cases"] + checked = 0 + for c in cases: + n = c["input_len"] + inp = pattern_input(n) + out_len = len(c["hash"]) // 2 # hex -> bytes (extended output length) + + got_hash = ref.blake3_hash(inp, out_len).hex() + assert got_hash == c["hash"], \ + f"[hash] len={n}: mismatch\n got={got_hash}\n exp={c['hash']}" + + got_keyed = ref.blake3_keyed_hash(key, inp, out_len).hex() + assert got_keyed == c["keyed_hash"], \ + f"[keyed] len={n}: mismatch\n got={got_keyed}\n exp={c['keyed_hash']}" + + got_dk = ref.blake3_derive_key(context, inp, out_len).hex() + assert got_dk == c["derive_key"], \ + f"[dkey] len={n}: mismatch\n got={got_dk}\n exp={c['derive_key']}" + + checked += 1 + return checked, len(cases), context + + +# --------------------------------------------------------------------------- +# ANCHOR 2: official `blake3` PyPI package (reference Rust impl) +# --------------------------------------------------------------------------- + +def test_pypi_blake3(): + try: + import blake3 as blake3_pkg + except ImportError: + return None # signal "unavailable" + + rng = random.Random(0xB3B3B3) + lengths = [0, 1, 2, 31, 32, 33, 63, 64, 65, 127, 128, 129, 512, 1000, 1023, + 1024, 1025, 2048, 4096, 4097, 10000, 65536, 100000] + n_checked = 0 + + # 2a. Default hash, default (32-byte) and extended output. + for n in lengths: + msg = bytes(rng.randrange(256) for _ in range(n)) + assert ref.blake3_hash(msg, 32) == blake3_pkg.blake3(msg).digest(), \ + f"pypi default hash mismatch at len={n}" + xof = rng.choice([16, 32, 64, 131, 200]) + assert ref.blake3_hash(msg, xof) == blake3_pkg.blake3(msg).digest(length=xof), \ + f"pypi XOF mismatch at len={n}, xof={xof}" + n_checked += 2 + + # 2b. Keyed hash. + for n in lengths: + key = bytes(rng.randrange(256) for _ in range(32)) + msg = bytes(rng.randrange(256) for _ in range(n)) + assert ref.blake3_keyed_hash(key, msg, 32) == \ + blake3_pkg.blake3(msg, key=key).digest(), f"pypi keyed mismatch at len={n}" + n_checked += 1 + + # 2c. Derive key. + for n in lengths: + ctx = f"lambda-vm blake3 oracle test context {n}" + material = bytes(rng.randrange(256) for _ in range(n)) + got = ref.blake3_derive_key(ctx, material, 32) + exp = blake3_pkg.blake3(material, derive_key_context=ctx).digest() + assert got == exp, f"pypi derive_key mismatch at len={n}" + n_checked += 1 + + return n_checked + + +# --------------------------------------------------------------------------- +# ANCHOR 3: Plonky3 blake3-air independent compression (flags = 0) +# +# Ported directly and independently from +# others/Plonky3/blake3-air/src/generation.rs +# (verifiable_half_round + generate_trace_row_for_round + feed-forward), which +# hardcodes flags = 0 and does exactly 7 rounds. This is a SECOND independent +# implementation of the compression function, checked at the compression level. +# --------------------------------------------------------------------------- + +# Plonky3 constants (constants.rs). IV stored as [lo16, hi16]. +_P3_IV = [ + (0x6A09 << 16) | 0xE667, (0xBB67 << 16) | 0xAE85, + (0x3C6E << 16) | 0xF372, (0xA54F << 16) | 0xF53A, + (0x510E << 16) | 0x527F, (0x9B05 << 16) | 0x688C, + (0x1F83 << 16) | 0xD9AB, (0x5BE0 << 16) | 0xCD19, +] +_P3_MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] + + +def _p3_permute(m): + return [m[_P3_MSG_PERMUTATION[i]] for i in range(16)] + + +def _p3_rotr(x, n): + x &= ref.MASK32 + return ((x >> n) | (x << (32 - n))) & ref.MASK32 + + +def _p3_half_round(a, b, c, d, m, flag): + # verifiable_half_round(generation.rs:203) + rot1, rot2 = (8, 7) if flag else (16, 12) + a = (a + b) & ref.MASK32 + a = (a + m) & ref.MASK32 + d = _p3_rotr(d ^ a, rot1) + c = (c + d) & ref.MASK32 + b = _p3_rotr(b ^ c, rot2) + return a, b, c, d + + +def _p3_round(state, m): + # generate_trace_row_for_round(generation.rs:120), state is [row][col]. + for i in range(4): # columns, first half + state[0][i], state[1][i], state[2][i], state[3][i] = _p3_half_round( + state[0][i], state[1][i], state[2][i], state[3][i], m[2 * i], False) + for i in range(4): # columns, second half + state[0][i], state[1][i], state[2][i], state[3][i] = _p3_half_round( + state[0][i], state[1][i], state[2][i], state[3][i], m[2 * i + 1], True) + for i in range(4): # diagonals, first half + state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], state[3][(i + 3) % 4] = \ + _p3_half_round(state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], + state[3][(i + 3) % 4], m[8 + 2 * i], False) + for i in range(4): # diagonals, second half + state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], state[3][(i + 3) % 4] = \ + _p3_half_round(state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], + state[3][(i + 3) % 4], m[9 + 2 * i], True) + + +def plonky3_compress(chaining_value, block_words, counter, block_len): + """Independent Plonky3 blake3-air compression. flags is hardcoded 0 + (v[15]=0), matching generation.rs. Returns 16 output words.""" + cv = list(chaining_value) + m = list(block_words) + state = [ + [cv[0], cv[1], cv[2], cv[3]], + [cv[4], cv[5], cv[6], cv[7]], + [_P3_IV[0], _P3_IV[1], _P3_IV[2], _P3_IV[3]], + [counter & ref.MASK32, (counter >> 32) & ref.MASK32, block_len & ref.MASK32, 0], + ] + for r in range(7): + _p3_round(state, m) + if r < 6: + m = _p3_permute(m) + out = [0] * 16 + for i in range(4): + out[i] = state[0][i] ^ state[2][i] + out[4 + i] = state[1][i] ^ state[3][i] + out[8 + i] = state[2][i] ^ cv[i] + out[12 + i] = state[3][i] ^ cv[4 + i] + return out + + +def test_plonky3_differential(): + rng = random.Random(0x9110C43) + n = 20000 + for _ in range(n): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + block_len = rng.randrange(0, 65) + mine = ref.compress(h, m, t, block_len, flags=0, rounds=7) + theirs = plonky3_compress(h, m, t, block_len) + assert mine == theirs, ( + f"Plonky3 differential mismatch\n h={h}\n m={m}\n t={t}\n " + f"block_len={block_len}\n mine={mine}\n theirs={theirs}") + return n + + +# --------------------------------------------------------------------------- +# Internal self-consistency (NOT an external anchor): compress_cv, feed-forward. +# --------------------------------------------------------------------------- + +def test_internal_consistency(): + rng = random.Random(7) + for _ in range(1000): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + full = ref.compress(h, m, t, bl, fl) + assert len(full) == 16 + assert ref.compress_cv(h, m, t, bl, fl) == full[:8] + # feed-forward invariant: output[8:16] = v[8:16] ^ h ; recompute v to check. + return 1000 + + +# --------------------------------------------------------------------------- +# 6-ROUND VARIANT: derivation check + canonical vectors. +# --------------------------------------------------------------------------- + +def test_6round_derivation(): + """Confirm the 6-round variant equals 7-round with the loop bound changed, + and that it genuinely differs from the 7-round function.""" + rng = random.Random(0x6) + differ = 0 + for _ in range(2000): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + v6a = ref.compress_6round(h, m, t, bl, fl) + v6b = ref.compress(h, m, t, bl, fl, rounds=6) + assert v6a == v6b, "compress_6round must equal compress(rounds=6)" + if ref.compress(h, m, t, bl, fl, rounds=7) != v6a: + differ += 1 + assert differ > 1990, "6-round and 7-round should differ on essentially all inputs" + return differ + + +def canonical_6round_vectors(): + """Deterministic canonical vectors for the 6-round variant (fixed seeds). + These become the variant's reference going forward (recorded in ORACLE.md).""" + vectors = [] + # 10 deterministic inputs derived from fixed seeds 0..9. + for seed in range(10): + rng = random.Random(seed) + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + out = ref.compress_6round(h, m, t, bl, fl) + vectors.append(dict(seed=seed, h=h, m=m, t=t, block_len=bl, flags=fl, out=out)) + return vectors + + +# --------------------------------------------------------------------------- + +def main(): + print("=" * 74) + print("BLAKE3 compression-function ORACLE — validation") + print("=" * 74) + + status = {"external_anchor": False} + + # Anchor 1 + checked, total, ctx = test_official_vectors() + print(f"[1] Official test_vectors.json : PASS ({checked}/{total} cases x 3 modes)") + print(f" modes: default hash, keyed hash, derive_key context={ctx!r}") + status["external_anchor"] = True + + # Anchor 2 + n2 = test_pypi_blake3() + if n2 is None: + print("[2] Official `blake3` PyPI pkg : SKIP (package not importable)") + else: + print(f"[2] Official `blake3` PyPI pkg : PASS ({n2} randomised differential checks, 3 modes)") + + # Anchor 3 + n3 = test_plonky3_differential() + print(f"[3] Plonky3 blake3-air (direct): PASS ({n3} random compressions, flags=0)") + + # Internal + ni = test_internal_consistency() + print(f"[.] Internal self-consistency : PASS ({ni} checks) [not an external anchor]") + + # 6-round + differ = test_6round_derivation() + print(f"[4] 6-round variant derivation : PASS (=compress(rounds=6); differs from 7r on {differ}/2000)") + + print("=" * 74) + print("VALIDATION STATUS: VALIDATED") + print(" 7-round reference: anchored on official test vectors + official") + print(" PyPI package + Plonky3 independent compression.") + print(" 6-round variant : derivative anchor (loop-bound diff) + canonical vectors below.") + print("=" * 74) + + # Emit canonical 6-round vectors. + print("\nCANONICAL 6-ROUND VARIANT VECTORS (seeds 0..9):") + vecs = canonical_6round_vectors() + out_json = os.path.join(HERE, "canonical_6round_vectors.json") + json.dump(vecs, open(out_json, "w"), indent=2) + for v in vecs: + out_hex = "".join(f"{w:08x}" for w in v["out"]) + print(f" seed={v['seed']}: t={v['t']:#018x} block_len={v['block_len']:2d} " + f"flags={v['flags']:#04x} -> out[0]={v['out'][0]:#010x} out[15]={v['out'][15]:#010x}") + print(f" (full vectors written to {os.path.basename(out_json)})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 19ed761b27a584585b27f02d92263c3402b68d3d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 29 Jul 2026 15:33:39 -0300 Subject: [PATCH 02/14] =?UTF-8?q?spike(blake3):=20restore=20the=20missing?= =?UTF-8?q?=20fixtures=20=E2=80=94=20oracle=20and=20gate=20now=20both=20ru?= =?UTF-8?q?n=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recovery left two fixtures absent because they had been downloaded or generated rather than written by a tool call, so no transcript held them. Both are now restored, and every claim in DESIGN.md §9 reproduces end to end: oracle [1] official vector set PASS 35/35 x 3 modes [2] blake3 PyPI package SKIP (not installed) [3] Plonky3 blake3-air PASS 20,000 compressions gate G-function UNSAT, init+feed-forward UNSAT, 5/5 negative controls SAT, 4/4 positive controls SAT (6-round seeds 0-2 and 7-round), width audit both bounds necessary. OVERALL: PASS The positive controls were the gap that mattered: they are the only check that would catch a per-instance wiring bug across the 48 unrolled G-functions, and they were unrunnable until now. PROVENANCE, because the label matters: official_test_vectors.json was REGENERATED from the official blake3 Rust crate v1.8.5 (generator kept in ground-truth/), not downloaded from upstream. It carries the official parameters — the Elvish key, the 2019-12-27 context string, the same 35 input lengths — and case 0 matches the independently known published digest. That is a genuine, non-circular anchor: the crate is the authors' reference implementation and is independent of blake3_ref.py. It is NOT the published artifact, and test_oracle.py still calls it "Official test_vectors.json". Read it as "checked against the official reference implementation using the official vector parameters". Keeping the generator in-tree means this is reproducible rather than another thing that evaporates. Two independent reviews (different models, no coordination) found no discrepancy in the primitive. One wrote a from-scratch BLAKE3 structured deliberately differently and differentially tested 100k random compressions, all 128 flag values x {6,7} rounds, a rounds sweep 0..8, and whole-hash over 227 lengths x 4 modes, with zero mismatches; it confirmed r < rounds-1, so the classic 6-permutes-for-7-rounds off-by-one is absent. Independently, blake3_ref.py reproduces the published known-answer vectors for "" and "abc" exactly, which pins IV, G, all four rotations, the permutation and its count, the feed-forward, the flag bits and LE packing in one shot. README records what review found and did not fix: - the harness prints "VALIDATION STATUS: VALIDATED ... anchored on official test vectors + official PyPI package + Plonky3" even when anchor 2 SKIPs; the status dict is written and never consulted. Observed firing. - test_internal_consistency documents a feed-forward recomputation it does not implement. - DESIGN's internal Blake3 bus omits the input-to-output timestamp binding that keccak — its own cited precedent — carries in both halves of its tuple. Without it two compressions can swap outputs with every bus still balancing. The gate models arithmetic only and cannot see it. --- thoughts/blake3/README.md | 165 +++--- .../canonical_6round_vectors.json | 522 ++++++++++++++++++ .../blake3-oracle/official_test_vectors.json | 334 +++++++++++ thoughts/blake3/ground-truth/Cargo.toml | 10 + thoughts/blake3/ground-truth/src/main.rs | 138 +++++ 5 files changed, 1103 insertions(+), 66 deletions(-) create mode 100644 thoughts/blake3/blake3-oracle/canonical_6round_vectors.json create mode 100644 thoughts/blake3/blake3-oracle/official_test_vectors.json create mode 100644 thoughts/blake3/ground-truth/Cargo.toml create mode 100644 thoughts/blake3/ground-truth/src/main.rs diff --git a/thoughts/blake3/README.md b/thoughts/blake3/README.md index 78a17680d..239993809 100644 --- a/thoughts/blake3/README.md +++ b/thoughts/blake3/README.md @@ -1,4 +1,4 @@ -# BLAKE3 accelerator — oracle + gate-proved chip design (RECOVERED) +# BLAKE3 accelerator — oracle + gate-proved chip design (RECOVERED, re-validated) **Provenance: recovered 2026-07-29 from subagent transcripts, not from a backup.** The original work (2026-07-23) was written to a session scratchpad @@ -8,84 +8,117 @@ and `Edit` tool calls out of `.claude/projects/.../1c23da47-.../subagents/agent-ablake3-{oracle,chip-design}-*.jsonl` (5 Writes + 11 Edits, every Edit applied cleanly — no partial replays). -Committing them is the point: this is the second EC/hash campaign whose -verification artifacts were nearly lost to a scratchpad. Anything worth keeping -belongs in the repo. +Committing them is the point: this is the second campaign whose verification +artifacts were nearly lost to a scratchpad. Anything worth keeping belongs in +the repo. ## What this is -A BLAKE3 compression-function accelerator that was taken to a **gate-proved -design** and stopped there, blocked on a protocol decision — sign-off on a named -"6-round collision resistance" assumption. No Rust was ever written. +A BLAKE3 compression-function accelerator taken to a **gate-proved design** and +stopped there, blocked on a protocol decision — sign-off on a named "6-round +collision resistance" assumption. No Rust chip was ever written. -Purpose is **internal** (Merkle/Fiat–Shamir replacement candidate; the 6-round -variant is the primary target, with K12 as precedent). The EVM has no BLAKE3 — -only the BLAKE2b-F precompile at 0x09 (EIP-152), which is variable-round and -rarely used, so it stays guest code. +Purpose is **internal** (Merkle / Fiat–Shamir replacement candidate; the 6-round +variant is the primary target, K12 as precedent). The EVM has no BLAKE3 — only +the BLAKE2b-F precompile at 0x09 (EIP-152), variable-round and rarely used, so +that stays guest code. ## Contents | file | what it is | |---|---| -| `blake3-oracle/blake3_ref.py` | independent reference implementation of the compression function | -| `blake3-oracle/test_oracle.py` | the three-anchor validation harness; also emits `canonical_6round_vectors.json` | +| `blake3-oracle/blake3_ref.py` | independent reference implementation | +| `blake3-oracle/test_oracle.py` | three-anchor validation harness; emits the canonical 6-round vectors | | `blake3-oracle/ORACLE.md` | anchor results and the contract map | -| `blake3-chip/DESIGN.md` | the chip design + §7 risk ledger | +| `blake3-oracle/official_test_vectors.json` | 35-case vector set — **see provenance note below** | +| `blake3-oracle/canonical_6round_vectors.json` | 10 pinned 6-round vectors, regenerated by the harness | +| `blake3-chip/DESIGN.md` | chip design + §7 risk ledger | | `blake3-chip/z3_blake_verify.py` | the soundness gate | - -## Status as recorded at the time - -Oracle **validated** against three independent anchors: the official -`test_vectors.json` (35×3), a differential against the `blake3` PyPI package -(92 cases), and Plonky3's `blake3-air` (20k compressions). - -Gate **PASS**: G-function UNSAT under free inputs (composition covers all -rounds, 6r and 7r), init/feed-forward UNSAT, 5 negative controls SAT, positive -controls pinned to oracle vectors. The field-level audit separated BV-observable -from field-only bug classes — dropping SLL `AreBytes` goes SAT, while dropping -carry booleanity goes SAT **only** in Int-mod-p, invisible to bitvectors. Same -phenomenon the keccak HWSL work hit. - -Layout: **1 row per compression, unrolled, ≈5,030 cell-equivalents for 6 -rounds ≈ 1/15 of keccak-f.** That figure corrected the oracle phase's own ¼–⅓ -estimate — blake would otherwise re-commit the 16-word message every row. -Contract map has **zero missing operations**: 32-bit XOR = 4× ByteAlu, -rotr16/8 are free byte relabels, rotr12/7 are HWSL shifts 4/9 (or inlined as -linear identities per the keccak rewrite), add = `emit_add_pair`. - -Design decision O1 resolved as option (c): two summed carry bits -`µ·(a+b+m−s−2^32(c1+c2))` at degree 2 plus booleanity at degree 3 — a ternary -carry is degree 4 and therefore illegal once µ-gated, and µ-gating is mandatory -because of all-zero padding and inlined IVs. - -## Verification of THIS recovery (2026-07-29) - -`z3_blake_verify.py` runs, and its **structural negative controls all fire -5/5** — `rot_wrong_amount`, `swap_g_operand`, `wrong_iv`, `drop_ff_xor`, -`wrong_msg_index` each SAT as intended. That is strong evidence the recovered -source is the real thing and not a plausible-looking reconstruction. - -**Two fixtures could not be recovered** because they were downloaded or -generated rather than written by a tool call, so they are absent from the -transcript: - -- `blake3-oracle/official_test_vectors.json` — from the upstream BLAKE3 repo -- `blake3-oracle/canonical_6round_vectors.json` — regenerated by running - `test_oracle.py`, which currently stops at the official-vectors anchor - -Consequently the gate's **positive** controls and anchors 1–2 have not been -re-run here. To fully re-validate: fetch the upstream vectors, `pip install -blake3`, run `test_oracle.py` (which writes the canonical vectors), then -re-run `z3_blake_verify.py`. Anchor 3 needs nothing external — Plonky3 is -already vendored at `others/Plonky3/blake3-air`. +| `ground-truth/` | tiny Rust generator that produced the vector set from the official `blake3` crate | + +## Re-validation, 2026-07-29 — everything runs and passes + +Both fixtures were missing from the recovery (they had been downloaded or +generated, so no tool call held them). Both are now restored, and **every claim +in `DESIGN.md` §9 reproduces**: + +``` +oracle: [1] official vector set PASS 35/35 x 3 modes + [2] blake3 PyPI package SKIP (not installed) + [3] Plonky3 blake3-air PASS 20,000 compressions +gate: G-function UNSAT (covers all G) : True + init+feed-forward UNSAT (rounds=0): True + negative controls all SAT : True (5/5) + positive controls all SAT : True (6-round seeds 0,1,2 + 7-round) + width audit (bound necessity) : True + OVERALL: PASS +``` + +Independently of the harness, `blake3_ref.py` reproduces the published +known-answer vectors exactly: `blake3("")` = `af1349b9f5f9…41f3262` and +`blake3("abc")` = `6437b3ac38…d5bd9d85`. That single check exercises the IV, the +G function, all four rotations, the permutation *and its count*, the +feed-forward, the flag bit values and little-endian packing at once. + +Two independent reviews (different models, no coordination) found **no +discrepancy in the primitive**. One wrote a from-scratch BLAKE3 structured +deliberately differently and differentially tested 100k random compressions, all +128 flag values × {6,7} rounds, a rounds sweep 0..8, and whole-hash over 227 +lengths × 4 modes — zero mismatches — and confirmed `r < rounds−1`, i.e. **6 +permutes for 7 rounds**, so the classic off-by-one is absent. + +### ⚠ Provenance of `official_test_vectors.json` + +It was **regenerated from the official `blake3` Rust crate v1.8.5** +(`ground-truth/`), not downloaded from the upstream repo. It carries the +official parameters — key `whats the Elvish word for friend`, context +`BLAKE3 2019-12-27 16:29:52 test vectors context`, the same 35 input lengths — +and case 0 matches the independently-known published digest. + +This is a **genuine, non-circular anchor**: the Rust crate is the BLAKE3 +authors' reference implementation and is entirely independent of +`blake3_ref.py`. But it is *not* the published artifact, and `test_oracle.py` +still labels it "Official test_vectors.json". Read it as "checked against the +official reference implementation using the official vector parameters". + +### Known harness defects (found by review, not yet fixed) + +1. `main()` prints `VALIDATION STATUS: VALIDATED … anchored on official test + vectors + official PyPI package + Plonky3` **even when anchor 2 SKIPs** — the + `status["external_anchor"]` dict is written and never consulted. Observed + firing. The per-anchor lines are honest; the summary overstates. +2. `test_internal_consistency` carries a comment describing a feed-forward + recomputation (*"recompute v to check"*) that **is not implemented** — it only + checks output length and the CV prefix. + +## Open findings against the DESIGN (review, 2026-07-29) + +1. **The internal `Blake3` bus has no input↔output binding.** §1.1 defines a + receive of `(h, m, t, block_len, flags)` and a separate send of `out[0..16]`, + both at multiplicity μ, and §3 lists `TIMESTAMP_0/1` as "bus binding + (internal variant **may omit**)". If omitted, with two compressions in a + trace a prover can have row A receive inputs_A and send out_B while row B + does the reverse: every tuple appears once on each side, **all buses + balance**, and both callers get the wrong answer. The design's own cited + precedent does not do this — keccak's internal bus carries + `TIMESTAMP_0, TIMESTAMP_1` in *both* halves (`keccak.rs`, send at round 0 / + receive at round 24). **Do not omit the timestamp.** The gate cannot catch + this: it models arithmetic only and has no bus layer. +2. **"Covers every G, hence every round" is a model argument.** MAIN 0 proves + one G under free inputs; in Rust, 48 G instances are emitted separately and a + wrong column index in instance #37 is not covered by it. The concrete + positive controls do cover it — keep them runnable, and prefer `--full`'s + monolithic UNSAT before shipping. +3. **The 3-op add carry encoding is ambiguous** — `(c1,c2) = (1,0)` and `(0,1)` + both encode carry 1. Harmless for soundness; noted so nobody "fixes" it. ## If this is picked up again -The blocking item is unchanged and is a **protocol decision, not an -engineering one**: the gate proves the chip matches the reference, *not* that -6 rounds are secure. That needs a named, signed assumption in the spec. +The blocking item is a **protocol decision, not an engineering one**: the gate +proves the chip matches the reference, *not* that 6 rounds are secure. That +needs a named, signed assumption in the spec. Note for anyone citing precedent: the EC `lincomb2` design study justified its -own NUMS assumption with "like blake3's 6-round assumption" — but no such -assumption was ever recorded in the spec, because this work never shipped. It -was a precedent for something that had not happened. +NUMS assumption with "like blake3's 6-round assumption" — but no such assumption +was ever recorded in the spec, because this work never shipped. It was a +precedent for something that had not happened. diff --git a/thoughts/blake3/blake3-oracle/canonical_6round_vectors.json b/thoughts/blake3/blake3-oracle/canonical_6round_vectors.json new file mode 100644 index 000000000..10ee8cbc1 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/canonical_6round_vectors.json @@ -0,0 +1,522 @@ +[ + { + "seed": 0, + "h": [ + 3626764237, + 1806341205, + 2195908194, + 2046968324, + 3900315155, + 2167613558, + 1210484339, + 3246154361 + ], + "m": [ + 3874773259, + 1332073689, + 3134603515, + 2937688618, + 432508404, + 1864753826, + 3921352636, + 2048741382, + 1118805955, + 60308648, + 3726325546, + 3738645480, + 2437440079, + 4155553746, + 1924014660, + 4006490763 + ], + "t": 13033757608824335107, + "block_len": 42, + "flags": 52, + "out": [ + 3470381567, + 3259559595, + 3171982207, + 2434484470, + 2453496512, + 3624177727, + 1500783166, + 2857307264, + 2908815487, + 3037433307, + 3879152609, + 869521091, + 1118447691, + 3315752744, + 2041348976, + 3076471061 + ] + }, + { + "seed": 1, + "h": [ + 3280387012, + 1095513148, + 1930549411, + 2798570523, + 3387541014, + 403123852, + 3589583794, + 1912923437 + ], + "m": [ + 4059906722, + 3871601465, + 131383004, + 2325348894, + 1001090105, + 92297589, + 2758633299, + 3693442237, + 2878940490, + 1302957853, + 3790218436, + 2170177477, + 148287319, + 3424825176, + 743061144, + 1609337231 + ], + "t": 14359731685826847253, + "block_len": 50, + "flags": 94, + "out": [ + 4071191273, + 2180888812, + 1086656188, + 1268894457, + 2666129712, + 1796871858, + 3910496071, + 2829038646, + 2734036659, + 310856722, + 813072437, + 3759806425, + 3202728316, + 3592162272, + 809631558, + 3753447717 + ] + }, + { + "seed": 2, + "h": [ + 242886303, + 364522461, + 3588440356, + 1323436024, + 2602510382, + 2606193617, + 4077622522, + 117874757 + ], + "m": [ + 1632151663, + 2258090960, + 2407373688, + 1014142328, + 102469680, + 1396478261, + 2191394736, + 3837860530, + 3422057796, + 3276568223, + 1519503515, + 4131333072, + 3238422834, + 2277860467, + 2104593779, + 3972123491 + ], + "t": 8504173462006699459, + "block_len": 58, + "flags": 124, + "out": [ + 1520873748, + 3386274828, + 2268646132, + 2891926386, + 2680601054, + 1060043663, + 2360846610, + 4103578245, + 1023812198, + 2132949004, + 2949933306, + 304921216, + 1147868525, + 2990135490, + 3286938319, + 2002726802 + ] + }, + { + "seed": 3, + "h": [ + 2337446730, + 2593816829, + 3596902313, + 1006443827, + 2045921456, + 646892613, + 2726705791, + 2247046192 + ], + "m": [ + 3183652505, + 275012945, + 2538753386, + 3717411168, + 3774472248, + 3956088670, + 4018314376, + 3774703581, + 418563100, + 583981819, + 931951836, + 1292897679, + 2512874164, + 2509342356, + 3883517040, + 3989790985 + ], + "t": 10579804601021778419, + "block_len": 36, + "flags": 31, + "out": [ + 4007210923, + 328045400, + 2438725180, + 326208257, + 3037127287, + 3191867341, + 897875462, + 3457968278, + 1392116149, + 1252158200, + 2970061409, + 743537389, + 2693293984, + 3933130730, + 1832113072, + 3693687142 + ] + }, + { + "seed": 4, + "h": [ + 1013818839, + 1701057193, + 665600858, + 285680177, + 3942586889, + 3286348376, + 2305023086, + 456053774 + ], + "m": [ + 3983477513, + 3464545456, + 3437897285, + 830799655, + 1330795424, + 3779789200, + 2602114036, + 2884935804, + 2173054921, + 763602979, + 2034044485, + 1289545638, + 3903568191, + 3789523705, + 2183442722, + 1777884721 + ], + "t": 5316417565031027709, + "block_len": 57, + "flags": 41, + "out": [ + 3389046179, + 2216925754, + 3888680557, + 866690006, + 165466574, + 2712732178, + 4102951254, + 2399377685, + 2315607722, + 4284158421, + 3072657499, + 773501543, + 1793536573, + 3003084712, + 1896007841, + 1969351479 + ] + }, + { + "seed": 5, + "h": [ + 2675342405, + 3185950873, + 4051686260, + 2787324501, + 3869338171, + 486215926, + 1059022248, + 2335435112 + ], + "m": [ + 2465058629, + 930847394, + 1200367645, + 3288765765, + 3423720279, + 2651938379, + 544169062, + 3742654890, + 4219466551, + 3746962816, + 1242556253, + 4129516530, + 879521323, + 2966284567, + 3838591282, + 1283288560 + ], + "t": 7653677975526109578, + "block_len": 18, + "flags": 67, + "out": [ + 346682991, + 270262248, + 2601144541, + 3997938779, + 2056340738, + 2008238187, + 1505739028, + 2712480509, + 3247758822, + 2303640909, + 2906048517, + 2417554421, + 375059928, + 1048950168, + 2028430931, + 3145353309 + ] + }, + { + "seed": 6, + "h": [ + 3530265750, + 1123655737, + 1940104, + 1602711601, + 3307725433, + 1171229348, + 3444200791, + 2929389929 + ], + "m": [ + 2945015643, + 3626164985, + 400010022, + 3437188107, + 3456510285, + 1250623880, + 4086115940, + 1547818437, + 3906320867, + 1552099921, + 2584484726, + 1307063374, + 2530408928, + 2255988210, + 2846451649, + 842776239 + ], + "t": 4108804320044427842, + "block_len": 26, + "flags": 3, + "out": [ + 849805168, + 3271909564, + 3519510472, + 4052162593, + 1913105236, + 2673574855, + 3059096669, + 2568909711, + 3012256441, + 251056470, + 2571889841, + 162028814, + 841094977, + 2913193055, + 1533365974, + 712663730 + ] + }, + { + "seed": 7, + "h": [ + 647892279, + 2795742288, + 2301595691, + 2179419893, + 161042648, + 1862494042, + 300026767, + 1823296038 + ], + "m": [ + 4070378921, + 1703729684, + 4192983756, + 3687093963, + 1243862422, + 776213899, + 2744112455, + 1599435267, + 884585951, + 1349251823, + 1946412080, + 1287489453, + 3411833895, + 1048386555, + 2467131055, + 2255701793 + ], + "t": 1350317716114554168, + "block_len": 53, + "flags": 42, + "out": [ + 2788339013, + 315507188, + 3524996285, + 1987664994, + 1810642625, + 3673881822, + 1405781943, + 2464695899, + 2067943261, + 3789991295, + 1966842759, + 3435464740, + 1773068141, + 3149656659, + 2026915971, + 4092802697 + ] + }, + { + "seed": 8, + "h": [ + 973694259, + 4133025703, + 542587089, + 3027165658, + 365867937, + 899355976, + 2756803948, + 1971964490 + ], + "m": [ + 1946188980, + 3567061697, + 384681428, + 1750902959, + 1109633622, + 270963824, + 1620083717, + 2838299811, + 1453582679, + 2969113350, + 3871375977, + 4063259978, + 832596604, + 2486621942, + 3783693026, + 3771309886 + ], + "t": 3645965004013668149, + "block_len": 19, + "flags": 91, + "out": [ + 2436573921, + 3354865794, + 1172422691, + 1864318850, + 548333301, + 3673300372, + 4072793263, + 3573011628, + 1151623047, + 4106489061, + 1631493012, + 147739614, + 1341160100, + 1164702434, + 543615615, + 1012557131 + ] + }, + { + "seed": 9, + "h": [ + 1603362544, + 595022250, + 27638352, + 2159432582, + 347096279, + 1627876803, + 3114132053, + 674984870 + ], + "m": [ + 1022254636, + 476516009, + 2535870938, + 1250600339, + 2895821580, + 901471249, + 1207677876, + 3476821989, + 3807057864, + 3776879099, + 2111885832, + 100859404, + 2563432515, + 2485498850, + 872106831, + 358645241 + ], + "t": 16927792517719413886, + "block_len": 64, + "flags": 21, + "out": [ + 4126052628, + 2238491576, + 700329201, + 1614539036, + 2494029070, + 687619623, + 3058576584, + 757884927, + 1778041274, + 211062928, + 3599623221, + 3465651495, + 3893106709, + 1833234406, + 2278011253, + 3515517844 + ] + } +] \ No newline at end of file diff --git a/thoughts/blake3/blake3-oracle/official_test_vectors.json b/thoughts/blake3/blake3-oracle/official_test_vectors.json new file mode 100644 index 000000000..77cd38adb --- /dev/null +++ b/thoughts/blake3/blake3-oracle/official_test_vectors.json @@ -0,0 +1,334 @@ +{ + "key": "whats the Elvish word for friend", + "context_string": "BLAKE3 2019-12-27 16:29:52 test vectors context", + "cases": [ + { + "input_len": 0, + "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d", + "keyed_hash": "92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f", + "derive_key": "2cc39783c223154fea8dfb7c1b1660f2ac2dcbd1c1de8277b0b0dd39b7e50d7d905630c8be290dfcf3e6842f13bddd573c098c3f17361f1f206b8cad9d088aa4a3f746752c6b0ce6a83b0da81d59649257cdf8eb3e9f7d4998e41021fac119deefb896224ac99f860011f73609e6e0e4540f93b273e56547dfd3aa1a035ba6689d89a0" + }, + { + "input_len": 1, + "hash": "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213c3a6cb8bf623e20cdb535f8d1a5ffb86342d9c0b64aca3bce1d31f60adfa137b358ad4d79f97b47c3d5e79f179df87a3b9776ef8325f8329886ba42f07fb138bb502f4081cbcec3195c5871e6c23e2cc97d3c69a613eba131e5f1351f3f1da786545e5", + "keyed_hash": "6d7878dfff2f485635d39013278ae14f1454b8c0a3a2d34bc1ab38228a80c95b6568c0490609413006fbd428eb3fd14e7756d90f73a4725fad147f7bf70fd61c4e0cf7074885e92b0e3f125978b4154986d4fb202a3f331a3fb6cf349a3a70e49990f98fe4289761c8602c4e6ab1138d31d3b62218078b2f3ba9a88e1d08d0dd4cea11", + "derive_key": "b3e2e340a117a499c6cf2398a19ee0d29cca2bb7404c73063382693bf66cb06c5827b91bf889b6b97c5477f535361caefca0b5d8c4746441c57617111933158950670f9aa8a05d791daae10ac683cbef8faf897c84e6114a59d2173c3f417023a35d6983f2c7dfa57e7fc559ad751dbfb9ffab39c2ef8c4aafebc9ae973a64f0c76551" + }, + { + "input_len": 2, + "hash": "7b7015bb92cf0b318037702a6cdd81dee41224f734684c2c122cd6359cb1ee63d8386b22e2ddc05836b7c1bb693d92af006deb5ffbc4c70fb44d0195d0c6f252faac61659ef86523aa16517f87cb5f1340e723756ab65efb2f91964e14391de2a432263a6faf1d146937b35a33621c12d00be8223a7f1919cec0acd12097ff3ab00ab1", + "keyed_hash": "5392ddae0e0a69d5f40160462cbd9bd889375082ff224ac9c758802b7a6fd20a9ffbf7efd13e989a6c246f96d3a96b9d279f2c4e63fb0bdff633957acf50ee1a5f658be144bab0f6f16500dee4aa5967fc2c586d85a04caddec90fffb7633f46a60786024353b9e5cebe277fcd9514217fee2267dcda8f7b31697b7c54fab6a939bf8f", + "derive_key": "1f166565a7df0098ee65922d7fea425fb18b9943f19d6161e2d17939356168e6daa59cae19892b2d54f6fc9f475d26031fd1c22ae0a3e8ef7bdb23f452a15e0027629d2e867b1bb1e6ab21c71297377750826c404dfccc2406bd57a83775f89e0b075e59a7732326715ef912078e213944f490ad68037557518b79c0086de6d6f6cdd2" + }, + { + "input_len": 3, + "hash": "e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f5b49b82f805a538c68915c1ae8035c900fd1d4b13902920fd05e1450822f36de9454b7e9996de4900c8e723512883f93f4345f8a58bfe64ee38d3ad71ab027765d25cdd0e448328a8e7a683b9a6af8b0af94fa09010d9186890b096a08471e4230a134", + "keyed_hash": "39e67b76b5a007d4921969779fe666da67b5213b096084ab674742f0d5ec62b9b9142d0fab08e1b161efdbb28d18afc64d8f72160c958e53a950cdecf91c1a1bbab1a9c0f01def762a77e2e8545d4dec241e98a89b6db2e9a5b070fc110caae2622690bd7b76c02ab60750a3ea75426a6bb8803c370ffe465f07fb57def95df772c39f", + "derive_key": "440aba35cb006b61fc17c0529255de438efc06a8c9ebf3f2ddac3b5a86705797f27e2e914574f4d87ec04c379e12789eccbfbc15892626042707802dbe4e97c3ff59dca80c1e54246b6d055154f7348a39b7d098b2b4824ebe90e104e763b2a447512132cede16243484a55a4e40a85790038bb0dcf762e8c053cabae41bbe22a5bff7" + }, + { + "input_len": 4, + "hash": "f30f5ab28fe047904037f77b6da4fea1e27241c5d132638d8bedce9d40494f328f603ba4564453e06cdcee6cbe728a4519bbe6f0d41e8a14b5b225174a566dbfa61b56afb1e452dc08c804f8c3143c9e2cc4a31bb738bf8c1917b55830c6e65797211701dc0b98daa1faeaa6ee9e56ab606ce03a1a881e8f14e87a4acf4646272cfd12", + "keyed_hash": "7671dde590c95d5ac9616651ff5aa0a27bee5913a348e053b8aa9108917fe070116c0acff3f0d1fa97ab38d813fd46506089118147d83393019b068a55d646251ecf81105f798d76a10ae413f3d925787d6216a7eb444e510fd56916f1d753a5544ecf0072134a146b2615b42f50c179f56b8fae0788008e3e27c67482349e249cb86a", + "derive_key": "f46085c8190d69022369ce1a18880e9b369c135eb93f3c63550d3e7630e91060fbd7d8f4258bec9da4e05044f88b91944f7cab317a2f0c18279629a3867fad0662c9ad4d42c6f27e5b124da17c8c4f3a94a025ba5d1b623686c6099d202a7317a82e3d95dae46a87de0555d727a5df55de44dab799a20dffe239594d6e99ed17950910" + }, + { + "input_len": 5, + "hash": "b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2ebcfdac6c5d32c31209e1f81a454751280db64942ce395104e1e4eaca62607de1c2ca748251754ea5bbe8c20150e7f47efd57012c63b3c6a6632dc1c7cd15f3e1c999904037d60fac2eb9397f2adbe458d7f264e64f1e73aa927b30988e2aed2f03620", + "keyed_hash": "73ac69eecf286894d8102018a6fc729f4b1f4247d3703f69bdc6a5fe3e0c84616ab199d1f2f3e53bffb17f0a2209fe8b4f7d4c7bae59c2bc7d01f1ff94c67588cc6b38fa6024886f2c078bfe09b5d9e6584cd6c521c3bb52f4de7687b37117a2dbbec0d59e92fa9a8cc3240d4432f91757aabcae03e87431dac003e7d73574bfdd8218", + "derive_key": "1f24eda69dbcb752847ec3ebb5dd42836d86e58500c7c98d906ecd82ed9ae47f6f48a3f67e4e43329c9a89b1ca526b9b35cbf7d25c1e353baffb590fd79be58ddb6c711f1a6b60e98620b851c688670412fcb0435657ba6b638d21f0f2a04f2f6b0bd8834837b10e438d5f4c7c2c71299cf7586ea9144ed09253d51f8f54dd6bff719d" + }, + { + "input_len": 6, + "hash": "06c4e8ffb6872fad96f9aaca5eee1553eb62aed0ad7198cef42e87f6a616c844611a30c4e4f37fe2fe23c0883cde5cf7059d88b657c7ed2087e3d210925ede716435d6d5d82597a1e52b9553919e804f5656278bd739880692c94bff2824d8e0b48cac1d24682699e4883389dc4f2faa2eb3b4db6e39debd5061ff3609916f3e07529a", + "keyed_hash": "82d3199d0013035682cc7f2a399d4c212544376a839aa863a0f4c91220ca7a6dc2ffb3aa05f2631f0fa9ac19b6e97eb7e6669e5ec254799350c8b8d189e8807800842a5383c4d907c932f34490aaf00064de8cdb157357bde37c1504d2960034930887603abc5ccb9f5247f79224baff6120a3c622a46d7b1bcaee02c5025460941256", + "derive_key": "be96b30b37919fe4379dfbe752ae77b4f7e2ab92f7ff27435f76f2f065f6a5f435ae01a1d14bd5a6b3b69d8cbd35f0b01ef2173ff6f9b640ca0bd4748efa398bf9a9c0acd6a66d9332fdc9b47ffe28ba7ab6090c26747b85f4fab22f936b71eb3f64613d8bd9dfabe9bb68da19de78321b481e5297df9e40ec8a3d662f3e1479c65de0" + }, + { + "input_len": 7, + "hash": "3f8770f387faad08faa9d8414e9f449ac68e6ff0417f673f602a646a891419fe66036ef6e6d1a8f54baa9fed1fc11c77cfb9cff65bae915045027046ebe0c01bf5a941f3bb0f73791d3fc0b84370f9f30af0cd5b0fc334dd61f70feb60dad785f070fef1f343ed933b49a5ca0d16a503f599a365a4296739248b28d1a20b0e2cc8975c", + "keyed_hash": "af0a7ec382aedc0cfd626e49e7628bc7a353a4cb108855541a5651bf64fbb28a7c5035ba0f48a9c73dabb2be0533d02e8fd5d0d5639a18b2803ba6bf527e1d145d5fd6406c437b79bcaad6c7bdf1cf4bd56a893c3eb9510335a7a798548c6753f74617bede88bef924ba4b334f8852476d90b26c5dc4c3668a2519266a562c6c8034a6", + "derive_key": "dc3b6485f9d94935329442916b0d059685ba815a1fa2a14107217453a7fc9f0e66266db2ea7c96843f9d8208e600a73f7f45b2f55b9e6d6a7ccf05daae63a3fdd10b25ac0bd2e224ce8291f88c05976d575df998477db86fb2cfbbf91725d62cb57acfeb3c2d973b89b503c2b60dde85a7802b69dc1ac2007d5623cbea8cbfb6b181f5" + }, + { + "input_len": 8, + "hash": "2351207d04fc16ade43ccab08600939c7c1fa70a5c0aaca76063d04c3228eaeb725d6d46ceed8f785ab9f2f9b06acfe398c6699c6129da084cb531177445a682894f9685eaf836999221d17c9a64a3a057000524cd2823986db378b074290a1a9b93a22e135ed2c14c7e20c6d045cd00b903400374126676ea78874d79f2dd7883cf5c", + "keyed_hash": "be2f5495c61cba1bb348a34948c004045e3bd4dae8f0fe82bf44d0da245a060048eb5e68ce6dea1eb0229e144f578b3aa7e9f4f85febd135df8525e6fe40c6f0340d13dd09b255ccd5112a94238f2be3c0b5b7ecde06580426a93e0708555a265305abf86d874e34b4995b788e37a823491f25127a502fe0704baa6bfdf04e76c13276", + "derive_key": "2b166978cef14d9d438046c720519d8b1cad707e199746f1562d0c87fbd32940f0e2545a96693a66654225ebbaac76d093bfa9cd8f525a53acb92a861a98c42e7d1c4ae82e68ab691d510012edd2a728f98cd4794ef757e94d6546961b4f280a51aac339cc95b64a92b83cc3f26d8af8dfb4c091c240acdb4d47728d23e7148720ef04" + }, + { + "input_len": 63, + "hash": "e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b1197012b1e7d9af4d7cb7bdd1f3bb49a90a9b5dec3ea2bbc6eaebce77f4e470cbf4687093b5352f04e4a4570fba233164e6acc36900e35d185886a827f7ea9bdc1e5c3ce88b095a200e62c10c043b3e9bc6cb9b6ac4dfa51794b02ace9f98779040755", + "keyed_hash": "bb1eb5d4afa793c1ebdd9fb08def6c36d10096986ae0cfe148cd101170ce37aea05a63d74a840aecd514f654f080e51ac50fd617d22610d91780fe6b07a26b0847abb38291058c97474ef6ddd190d30fc318185c09ca1589d2024f0a6f16d45f11678377483fa5c005b2a107cb9943e5da634e7046855eaa888663de55d6471371d55d", + "derive_key": "b6451e30b953c206e34644c6803724e9d2725e0893039cfc49584f991f451af3b89e8ff572d3da4f4022199b9563b9d70ebb616efff0763e9abec71b550f1371e233319c4c4e74da936ba8e5bbb29a598e007a0bbfa929c99738ca2cc098d59134d11ff300c39f82e2fce9f7f0fa266459503f64ab9913befc65fddc474f6dc1c67669" + }, + { + "input_len": 64, + "hash": "4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98fc9cc56cb831ffe33ea8e7e1d1df09b26efd2767670066aa82d023b1dfe8ab1b2b7fbb5b97592d46ffe3e05a6a9b592e2949c74160e4674301bc3f97e04903f8c6cf95b863174c33228924cdef7ae47559b10b294acd660666c4538833582b43f82d74", + "keyed_hash": "ba8ced36f327700d213f120b1a207a3b8c04330528586f414d09f2f7d9ccb7e68244c26010afc3f762615bbac552a1ca909e67c83e2fd5478cf46b9e811efccc93f77a21b17a152ebaca1695733fdb086e23cd0eb48c41c034d52523fc21236e5d8c9255306e48d52ba40b4dac24256460d56573d1312319afcf3ed39d72d0bfc69acb", + "derive_key": "a5c4a7053fa86b64746d4bb688d06ad1f02a18fce9afd3e818fefaa7126bf73e9b9493a9befebe0bf0c9509fb3105cfa0e262cde141aa8e3f2c2f77890bb64a4cca96922a21ead111f6338ad5244f2c15c44cb595443ac2ac294231e31be4a4307d0a91e874d36fc9852aeb1265c09b6e0cda7c37ef686fbbcab97e8ff66718be048bb" + }, + { + "input_len": 65, + "hash": "de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee0e16e0a4749d6811dd1d6d1265c29729b1b75a9ac346cf93f0e1d7296dfcfd4313b3a227faaaaf7757cc95b4e87a49be3b8a270a12020233509b1c3632b3485eef309d0abc4a4a696c9decc6e90454b53b000f456a3f10079072baaf7a981653221f2c", + "keyed_hash": "c0a4edefa2d2accb9277c371ac12fcdbb52988a86edc54f0716e1591b4326e72d5e795f46a596b02d3d4bfb43abad1e5d19211152722ec1f20fef2cd413e3c22f2fc5da3d73041275be6ede3517b3b9f0fc67ade5956a672b8b75d96cb43294b9041497de92637ed3f2439225e683910cb3ae923374449ca788fb0f9bea92731bc26ad", + "derive_key": "51fd05c3c1cfbc8ed67d139ad76f5cf8236cd2acd26627a30c104dfd9d3ff8a82b02e8bd36d8498a75ad8c8e9b15eb386970283d6dd42c8ae7911cc592887fdbe26a0a5f0bf821cd92986c60b2502c9be3f98a9c133a7e8045ea867e0828c7252e739321f7c2d65daee4468eb4429efae469a42763f1f94977435d10dccae3e3dce88d" + }, + { + "input_len": 127, + "hash": "d81293fda863f008c09e92fc382a81f5a0b4a1251cba1634016a0f86a6bd640de3137d477156d1fde56b0cf36f8ef18b44b2d79897bece12227539ac9ae0a5119da47644d934d26e74dc316145dcb8bb69ac3f2e05c242dd6ee06484fcb0e956dc44355b452c5e2bbb5e2b66e99f5dd443d0cbcaaafd4beebaed24ae2f8bb672bcef78", + "keyed_hash": "c64200ae7dfaf35577ac5a9521c47863fb71514a3bcad18819218b818de85818ee7a317aaccc1458f78d6f65f3427ec97d9c0adb0d6dacd4471374b621b7b5f35cd54663c64dbe0b9e2d95632f84c611313ea5bd90b71ce97b3cf645776f3adc11e27d135cbadb9875c2bf8d3ae6b02f8a0206aba0c35bfe42574011931c9a255ce6dc", + "derive_key": "c91c090ceee3a3ac81902da31838012625bbcd73fcb92e7d7e56f78deba4f0c3feeb3974306966ccb3e3c69c337ef8a45660ad02526306fd685c88542ad00f759af6dd1adc2e50c2b8aac9f0c5221ff481565cf6455b772515a69463223202e5c371743e35210bbbbabd89651684107fd9fe493c937be16e39cfa7084a36207c99bea3" + }, + { + "input_len": 128, + "hash": "f17e570564b26578c33bb7f44643f539624b05df1a76c81f30acd548c44b45efa69faba091427f9c5c4caa873aa07828651f19c55bad85c47d1368b11c6fd99e47ecba5820a0325984d74fe3e4058494ca12e3f1d3293d0010a9722f7dee64f71246f75e9361f44cc8e214a100650db1313ff76a9f93ec6e84edb7add1cb4a95019b0c", + "keyed_hash": "b04fe15577457267ff3b6f3c947d93be581e7e3a4b018679125eaf86f6a628ecd86bbe0001f10bda47e6077b735016fca8119da11348d93ca302bbd125bde0db2b50edbe728a620bb9d3e6f706286aedea973425c0b9eedf8a38873544cf91badf49ad92a635a93f71ddfcee1eae536c25d1b270956be16588ef1cfef2f1d15f650bd5", + "derive_key": "81720f34452f58a0120a58b6b4608384b5c51d11f39ce97161a0c0e442ca022550e7cd651e312f0b4c6afb3c348ae5dd17d2b29fab3b894d9a0034c7b04fd9190cbd90043ff65d1657bbc05bfdecf2897dd894c7a1b54656d59a50b51190a9da44db426266ad6ce7c173a8c0bbe091b75e734b4dadb59b2861cd2518b4e7591e4b83c9" + }, + { + "input_len": 129, + "hash": "683aaae9f3c5ba37eaaf072aed0f9e30bac0865137bae68b1fde4ca2aebdcb12f96ffa7b36dd78ba321be7e842d364a62a42e3746681c8bace18a4a8a79649285c7127bf8febf125be9de39586d251f0d41da20980b70d35e3dac0eee59e468a894fa7e6a07129aaad09855f6ad4801512a116ba2b7841e6cfc99ad77594a8f2d181a7", + "keyed_hash": "d4a64dae6cdccbac1e5287f54f17c5f985105457c1a2ec1878ebd4b57e20d38f1c9db018541eec241b748f87725665b7b1ace3e0065b29c3bcb232c90e37897fa5aaee7e1e8a2ecfcd9b51463e42238cfdd7fee1aecb3267fa7f2128079176132a412cd8aaf0791276f6b98ff67359bd8652ef3a203976d5ff1cd41885573487bcd683", + "derive_key": "938d2d4435be30eafdbb2b7031f7857c98b04881227391dc40db3c7b21f41fc18d72d0f9c1de5760e1941aebf3100b51d64644cb459eb5d20258e233892805eb98b07570ef2a1787cd48e117c8d6a63a68fd8fc8e59e79dbe63129e88352865721c8d5f0cf183f85e0609860472b0d6087cefdd186d984b21542c1c780684ed6832d8d" + }, + { + "input_len": 1023, + "hash": "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11a182d27a591b05592b15607500e1e8dd56bc6c7fc063715b7a1d737df5bad3339c56778957d870eb9717b57ea3d9fb68d1b55127bba6a906a4a24bbd5acb2d123a37b28f9e9a81bbaae360d58f85e5fc9d75f7c370a0cc09b6522d9c8d822f2f28f485", + "keyed_hash": "c951ecdf03288d0fcc96ee3413563d8a6d3589547f2c2fb36d9786470f1b9d6e890316d2e6d8b8c25b0a5b2180f94fb1a158ef508c3cde45e2966bd796a696d3e13efd86259d756387d9becf5c8bf1ce2192b87025152907b6d8cc33d17826d8b7b9bc97e38c3c85108ef09f013e01c229c20a83d9e8efac5b37470da28575fd755a10", + "derive_key": "74a16c1c3d44368a86e1ca6df64be6a2f64cce8f09220787450722d85725dea59c413264404661e9e4d955409dfe4ad3aa487871bcd454ed12abfe2c2b1eb7757588cf6cb18d2eccad49e018c0d0fec323bec82bf1644c6325717d13ea712e6840d3e6e730d35553f59eff5377a9c350bcc1556694b924b858f329c44ee64b884ef00d" + }, + { + "input_len": 1024, + "hash": "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af71cf8107265ecdaf8505b95d8fcec83a98a6a96ea5109d2c179c47a387ffbb404756f6eeae7883b446b70ebb144527c2075ab8ab204c0086bb22b7c93d465efc57f8d917f0b385c6df265e77003b85102967486ed57db5c5ca170ba441427ed9afa684e", + "keyed_hash": "75c46f6f3d9eb4f55ecaaee480db732e6c2105546f1e675003687c31719c7ba4a78bc838c72852d4f49c864acb7adafe2478e824afe51c8919d06168414c265f298a8094b1ad813a9b8614acabac321f24ce61c5a5346eb519520d38ecc43e89b5000236df0597243e4d2493fd626730e2ba17ac4d8824d09d1a4a8f57b8227778e2de", + "derive_key": "7356cd7720d5b66b6d0697eb3177d9f8d73a4a5c5e968896eb6a6896843027066c23b601d3ddfb391e90d5c8eccdef4ae2a264bce9e612ba15e2bc9d654af1481b2e75dbabe615974f1070bba84d56853265a34330b4766f8e75edd1f4a1650476c10802f22b64bd3919d246ba20a17558bc51c199efdec67e80a227251808d8ce5bad" + }, + { + "input_len": 1025, + "hash": "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444f4c4a22b4b399155358a994e52bf255de60035742ec71bd08ac275a1b51cc6bfe332b0ef84b409108cda080e6269ed4b3e2c3f7d722aa4cdc98d16deb554e5627be8f955c98e1d5f9565a9194cad0c4285f93700062d9595adb992ae68ff12800ab67a", + "keyed_hash": "357dc55de0c7e382c900fd6e320acc04146be01db6a8ce7210b7189bd664ea69362396b77fdc0d2634a552970843722066c3c15902ae5097e00ff53f1e116f1cd5352720113a837ab2452cafbde4d54085d9cf5d21ca613071551b25d52e69d6c81123872b6f19cd3bc1333edf0c52b94de23ba772cf82636cff4542540a7738d5b930", + "derive_key": "effaa245f065fbf82ac186839a249707c3bddf6d3fdda22d1b95a3c970379bcb5d31013a167509e9066273ab6e2123bc835b408b067d88f96addb550d96b6852dad38e320b9d940f86db74d398c770f462118b35d2724efa13da97194491d96dd37c3c09cbef665953f2ee85ec83d88b88d11547a6f911c8217cca46defa2751e7f3ad" + }, + { + "input_len": 2048, + "hash": "e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a9a60bf80001410ec9eea6698cd537939fad4749edd484cb541aced55cd9bf54764d063f23f6f1e32e12958ba5cfeb1bf618ad094266d4fc3c968c2088f677454c288c67ba0dba337b9d91c7e1ba586dc9a5bc2d5e90c14f53a8863ac75655461cea8f9", + "keyed_hash": "879cf1fa2ea0e79126cb1063617a05b6ad9d0b696d0d757cf053439f60a99dd10173b961cd574288194b23ece278c330fbb8585485e74967f31352a8183aa782b2b22f26cdcadb61eed1a5bc144b8198fbb0c13abbf8e3192c145d0a5c21633b0ef86054f42809df823389ee40811a5910dcbd1018af31c3b43aa55201ed4edaac74fe", + "derive_key": "7b2945cb4fef70885cc5d78a87bf6f6207dd901ff239201351ffac04e1088a23e2c11a1ebffcea4d80447867b61badb1383d842d4e79645d48dd82ccba290769caa7af8eaa1bd78a2a5e6e94fbdab78d9c7b74e894879f6a515257ccf6f95056f4e25390f24f6b35ffbb74b766202569b1d797f2d4bd9d17524c720107f985f4ddc583" + }, + { + "input_len": 2049, + "hash": "5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b687952256303096de31d71d74103403822a2e0bc1eb193e7aecc9643a76b7bbc0c9f9c52e8783aae98764ca468962b5c2ec92f0c74eb5448d519713e09413719431c802f948dd5d90425a4ecdadece9eb178d80f26efccae630734dff63340285adec2aed3b51073ad3", + "keyed_hash": "9f29700902f7c86e514ddc4df1e3049f258b2472b6dd5267f61bf13983b78dd5f9a88abfefdfa1e00b418971f2b39c64ca621e8eb37fceac57fd0c8fc8e117d43b81447be22d5d8186f8f5919ba6bcc6846bd7d50726c06d245672c2ad4f61702c646499ee1173daa061ffe15bf45a631e2946d616a4c345822f1151284712f76b2b0e", + "derive_key": "2ea477c5515cc3dd606512ee72bb3e0e758cfae7232826f35fb98ca1bcbdf27316d8e9e79081a80b046b60f6a263616f33ca464bd78d79fa18200d06c7fc9bffd808cc4755277a7d5e09da0f29ed150f6537ea9bed946227ff184cc66a72a5f8c1e4bd8b04e81cf40fe6dc4427ad5678311a61f4ffc39d195589bdbc670f63ae70f4b6" + }, + { + "input_len": 3072, + "hash": "b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd29a3f6b0b978d6608335c09dc94ccf682f9951cdfc501bfe47b9c9189a6fc7b404d120258506341a6d802857322fbd20d3e5dae05b95c88793fa83db1cb08e7d8008d1599b6209d78336e24839724c191b2a52a80448306e0daa84a3fdb566661a37e11", + "keyed_hash": "044a0e7b172a312dc02a4c9a818c036ffa2776368d7f528268d2e6b5df19177022f302d0529e4174cc507c463671217975e81dab02b8fdeb0d7ccc7568dd22574c783a76be215441b32e91b9a904be8ea81f7a0afd14bad8ee7c8efc305ace5d3dd61b996febe8da4f56ca0919359a7533216e2999fc87ff7d8f176fbecb3d6f34278b", + "derive_key": "050df97f8c2ead654d9bb3ab8c9178edcd902a32f8495949feadcc1e0480c46b3604131bbd6e3ba573b6dd682fa0a63e5b165d39fc43a625d00207607a2bfeb65ff1d29292152e26b298868e3b87be95d6458f6f2ce6118437b632415abe6ad522874bcd79e4030a5e7bad2efa90a7a7c67e93f0a18fb28369d0a9329ab5c24134ccb0" + }, + { + "input_len": 3073, + "hash": "7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd39a27ae3b79d68d89da9bf25bc27139ae65a324918a5f9b7828181e52cf373c84f35b639b7fccbb985b6f2fa56aea0c18f531203497b8bbd3a07ceb5926f1cab74d14bd66486d9a91eba99059a98bd1cd25876b2af5a76c3e9eed554ed72ea952b603bf", + "keyed_hash": "68dede9bef00ba89e43f31a6825f4cf433389fedae75c04ee9f0cf16a427c95a96d6da3fe985054d3478865be9a092250839a697bbda74e279e8a9e69f0025e4cfddd6cfb434b1cd9543aaf97c635d1b451a4386041e4bb100f5e45407cbbc24fa53ea2de3536ccb329e4eb9466ec37093a42cf62b82903c696a93a50b702c80f3c3c5", + "derive_key": "72613c9ec9ff7e40f8f5c173784c532ad852e827dba2bf85b2ab4b76f7079081576288e552647a9d86481c2cae75c2dd4e7c5195fb9ada1ef50e9c5098c249d743929191441301c69e1f48505a4305ec1778450ee48b8e69dc23a25960fe33070ea549119599760a8a2d28aeca06b8c5e9ba58bc19e11fe57b6ee98aa44b2a8e6b14a5" + }, + { + "input_len": 4096, + "hash": "015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e9690289e9409ddb1b99768eafe1623da896faf7e1114bebeadc1be30829b6f8af707d85c298f4f0ff4d9438aef948335612ae921e76d411c3a9111df62d27eaf871959ae0062b5492a0feb98ef3ed4af277f5395172dbe5c311918ea0074ce0036454f620", + "keyed_hash": "befc660aea2f1718884cd8deb9902811d332f4fc4a38cf7c7300d597a081bfc0bbb64a36edb564e01e4b4aaf3b060092a6b838bea44afebd2deb8298fa562b7b597c757b9df4c911c3ca462e2ac89e9a787357aaf74c3b56d5c07bc93ce899568a3eb17d9250c20f6c5f6c1e792ec9a2dcb715398d5a6ec6d5c54f586a00403a1af1de", + "derive_key": "1e0d7f3db8c414c97c6307cbda6cd27ac3b030949da8e23be1a1a924ad2f25b9d78038f7b198596c6cc4a9ccf93223c08722d684f240ff6569075ed81591fd93f9fff1110b3a75bc67e426012e5588959cc5a4c192173a03c00731cf84544f65a2fb9378989f72e9694a6a394a8a30997c2e67f95a504e631cd2c5f55246024761b245" + }, + { + "input_len": 4097, + "hash": "9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb99505f91b0b5600a11251652eacfa9497b31cd3c409ce2e45cfe6c0a016967316c426bd26f619eab5d70af9a418b845c608840390f361630bd497b1ab44019316357c61dbe091ce72fc16dc340ac3d6e009e050b3adac4b5b2c92e722cffdc46501531956", + "keyed_hash": "00df940cd36bb9fa7cbbc3556744e0dbc8191401afe70520ba292ee3ca80abbc606db4976cfdd266ae0abf667d9481831ff12e0caa268e7d3e57260c0824115a54ce595ccc897786d9dcbf495599cfd90157186a46ec800a6763f1c59e36197e9939e900809f7077c102f888caaf864b253bc41eea812656d46742e4ea42769f89b83f", + "derive_key": "aca51029626b55fda7117b42a7c211f8c6e9ba4fe5b7a8ca922f34299500ead8a897f66a400fed9198fd61dd2d58d382458e64e100128075fc54b860934e8de2e84170734b06e1d212a117100820dbc48292d148afa50567b8b84b1ec336ae10d40c8c975a624996e12de31abbe135d9d159375739c333798a80c64ae895e51e22f3ad" + }, + { + "input_len": 5120, + "hash": "9cadc15fed8b5d854562b26a9536d9707cadeda9b143978f319ab34230535833acc61c8fdc114a2010ce8038c853e121e1544985133fccdd0a2d507e8e615e611e9a0ba4f47915f49e53d721816a9198e8b30f12d20ec3689989175f1bf7a300eee0d9321fad8da232ece6efb8e9fd81b42ad161f6b9550a069e66b11b40487a5f5059", + "keyed_hash": "2c493e48e9b9bf31e0553a22b23503c0a3388f035cece68eb438d22fa1943e209b4dc9209cd80ce7c1f7c9a744658e7e288465717ae6e56d5463d4f80cdb2ef56495f6a4f5487f69749af0c34c2cdfa857f3056bf8d807336a14d7b89bf62bef2fb54f9af6a546f818dc1e98b9e07f8a5834da50fa28fb5874af91bf06020d1bf0120e", + "derive_key": "7a7acac8a02adcf3038d74cdd1d34527de8a0fcc0ee3399d1262397ce5817f6055d0cefd84d9d57fe792d65a278fd20384ac6c30fdb340092f1a74a92ace99c482b28f0fc0ef3b923e56ade20c6dba47e49227166251337d80a037e987ad3a7f728b5ab6dfafd6e2ab1bd583a95d9c895ba9c2422c24ea0f62961f0dca45cad47bfa0d" + }, + { + "input_len": 5121, + "hash": "628bd2cb2004694adaab7bbd778a25df25c47b9d4155a55f8fbd79f2fe154cff96adaab0613a6146cdaabe498c3a94e529d3fc1da2bd08edf54ed64d40dcd6777647eac51d8277d70219a9694334a68bc8f0f23e20b0ff70ada6f844542dfa32cd4204ca1846ef76d811cdb296f65e260227f477aa7aa008bac878f72257484f2b6c95", + "keyed_hash": "6ccf1c34753e7a044db80798ecd0782a8f76f33563accaddbfbb2e0ea4b2d0240d07e63f13667a8d1490e5e04f13eb617aea16a8c8a5aaed1ef6fbde1b0515e3c81050b361af6ead126032998290b563e3caddeaebfab592e155f2e161fb7cba939092133f23f9e65245e58ec23457b78a2e8a125588aad6e07d7f11a85b88d375b72d", + "derive_key": "b07f01e518e702f7ccb44a267e9e112d403a7b3f4883a47ffbed4b48339b3c341a0add0ac032ab5aaea1e4e5b004707ec5681ae0fcbe3796974c0b1cf31a194740c14519273eedaabec832e8a784b6e7cfc2c5952677e6c3f2c3914454082d7eb1ce1766ac7d75a4d3001fc89544dd46b5147382240d689bbbaefc359fb6ae30263165" + }, + { + "input_len": 6144, + "hash": "3e2e5b74e048f3add6d21faab3f83aa44d3b2278afb83b80b3c35164ebeca2054d742022da6fdda444ebc384b04a54c3ac5839b49da7d39f6d8a9db03deab32aade156c1c0311e9b3435cde0ddba0dce7b26a376cad121294b689193508dd63151603c6ddb866ad16c2ee41585d1633a2cea093bea714f4c5d6b903522045b20395c83", + "keyed_hash": "3d6b6d21281d0ade5b2b016ae4034c5dec10ca7e475f90f76eac7138e9bc8f1dc35754060091dc5caf3efabe0603c60f45e415bb3407db67e6beb3d11cf8e4f7907561f05dace0c15807f4b5f389c841eb114d81a82c02a00b57206b1d11fa6e803486b048a5ce87105a686dee041207e095323dfe172df73deb8c9532066d88f9da7e", + "derive_key": "2a95beae63ddce523762355cf4b9c1d8f131465780a391286a5d01abb5683a1597099e3c6488aab6c48f3c15dbe1942d21dbcdc12115d19a8b8465fb54e9053323a9178e4275647f1a9927f6439e52b7031a0b465c861a3fc531527f7758b2b888cf2f20582e9e2c593709c0a44f9c6e0f8b963994882ea4168827823eef1f64169fef" + }, + { + "input_len": 6145, + "hash": "f1323a8631446cc50536a9f705ee5cb619424d46887f3c376c695b70e0f0507f18a2cfdd73c6e39dd75ce7c1c6e3ef238fd54465f053b25d21044ccb2093beb015015532b108313b5829c3621ce324b8e14229091b7c93f32db2e4e63126a377d2a63a3597997d4f1cba59309cb4af240ba70cebff9a23d5e3ff0cdae2cfd54e070022", + "keyed_hash": "9ac301e9e39e45e3250a7e3b3df701aa0fb6889fbd80eeecf28dbc6300fbc539f3c184ca2f59780e27a576c1d1fb9772e99fd17881d02ac7dfd39675aca918453283ed8c3169085ef4a466b91c1649cc341dfdee60e32231fc34c9c4e0b9a2ba87ca8f372589c744c15fd6f985eec15e98136f25beeb4b13c4e43dc84abcc79cd4646c", + "derive_key": "379bcc61d0051dd489f686c13de00d5b14c505245103dc040d9e4dd1facab8e5114493d029bdbd295aaa744a59e31f35c7f52dba9c3642f773dd0b4262a9980a2aef811697e1305d37ba9d8b6d850ef07fe41108993180cf779aeece363704c76483458603bbeeb693cffbbe5588d1f3535dcad888893e53d977424bb707201569a8d2" + }, + { + "input_len": 7168, + "hash": "61da957ec2499a95d6b8023e2b0e604ec7f6b50e80a9678b89d2628e99ada77a5707c321c83361793b9af62a40f43b523df1c8633cecb4cd14d00bdc79c78fca5165b863893f6d38b02ff7236c5a9a8ad2dba87d24c547cab046c29fc5bc1ed142e1de4763613bb162a5a538e6ef05ed05199d751f9eb58d332791b8d73fb74e4fce95", + "keyed_hash": "b42835e40e9d4a7f42ad8cc04f85a963a76e18198377ed84adddeaecacc6f3fca2f01d5277d69bb681c70fa8d36094f73ec06e452c80d2ff2257ed82e7ba348400989a65ee8daa7094ae0933e3d2210ac6395c4af24f91c2b590ef87d7788d7066ea3eaebca4c08a4f14b9a27644f99084c3543711b64a070b94f2c9d1d8a90d035d52", + "derive_key": "11c37a112765370c94a51415d0d651190c288566e295d505defdad895dae223730d5a5175a38841693020669c7638f40b9bc1f9f39cf98bda7a5b54ae24218a800a2116b34665aa95d846d97ea988bfcb53dd9c055d588fa21ba78996776ea6c40bc428b53c62b5f3ccf200f647a5aae8067f0ea1976391fcc72af1945100e2a6dcb88" + }, + { + "input_len": 7169, + "hash": "a003fc7a51754a9b3c7fae0367ab3d782dccf28855a03d435f8cfe74605e781798a8b20534be1ca9eb2ae2df3fae2ea60e48c6fb0b850b1385b5de0fe460dbe9d9f9b0d8db4435da75c601156df9d047f4ede008732eb17adc05d96180f8a73548522840779e6062d643b79478a6e8dbce68927f36ebf676ffa7d72d5f68f050b119c8", + "keyed_hash": "ed9b1a922c046fdb3d423ae34e143b05ca1bf28b710432857bf738bcedbfa5113c9e28d72fcbfc020814ce3f5d4fc867f01c8f5b6caf305b3ea8a8ba2da3ab69fabcb438f19ff11f5378ad4484d75c478de425fb8e6ee809b54eec9bdb184315dc856617c09f5340451bf42fd3270a7b0b6566169f242e533777604c118a6358250f54", + "derive_key": "554b0a5efea9ef183f2f9b931b7497995d9eb26f5c5c6dad2b97d62fc5ac31d99b20652c016d88ba2a611bbd761668d5eda3e568e940faae24b0d9991c3bd25a65f770b89fdcadabcb3d1a9c1cb63e69721cacf1ae69fefdcef1e3ef41bc5312ccc17222199e47a26552c6adc460cf47a72319cb5039369d0060eaea59d6c65130f1dd" + }, + { + "input_len": 8192, + "hash": "aae792484c8efe4f19e2ca7d371d8c467ffb10748d8a5a1ae579948f718a2a635fe51a27db045a567c1ad51be5aa34c01c6651c4d9b5b5ac5d0fd58cf18dd61a47778566b797a8c67df7b1d60b97b19288d2d877bb2df417ace009dcb0241ca1257d62712b6a4043b4ff33f690d849da91ea3bf711ed583cb7b7a7da2839ba71309bbf", + "keyed_hash": "dc9637c8845a770b4cbf76b8daec0eebf7dc2eac11498517f08d44c8fc00d58a4834464159dcbc12a0ba0c6d6eb41bac0ed6585cabfe0aca36a375e6c5480c22afdc40785c170f5a6b8a1107dbee282318d00d915ac9ed1143ad40765ec120042ee121cd2baa36250c618adaf9e27260fda2f94dea8fb6f08c04f8f10c78292aa46102", + "derive_key": "ad01d7ae4ad059b0d33baa3c01319dcf8088094d0359e5fd45d6aeaa8b2d0c3d4c9e58958553513b67f84f8eac653aeeb02ae1d5672dcecf91cd9985a0e67f4501910ecba25555395427ccc7241d70dc21c190e2aadee875e5aae6bf1912837e53411dabf7a56cbf8e4fb780432b0d7fe6cec45024a0788cf5874616407757e9e6bef7" + }, + { + "input_len": 8193, + "hash": "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3bb2282aa69be089359ea1154b9a9286c4a56af4de975a9aa4a5c497654914d279bea60bb6d2cf7225a2fa0ff5ef56bbe4b149f3ed15860f78b4e2ad04e158e375c1e0c0b551cd7dfc82f1b155c11b6b3ed51ec9edb30d133653bb5709d1dbd55f4e1ff6", + "keyed_hash": "954a2a75420c8d6547e3ba5b98d963e6fa6491addc8c023189cc519821b4a1f5f03228648fd983aef045c2fa8290934b0866b615f585149587dda2299039965328835a2b18f1d63b7e300fc76ff260b571839fe44876a4eae66cbac8c67694411ed7e09df51068a22c6e67d6d3dd2cca8ff12e3275384006c80f4db68023f24eebba57", + "derive_key": "af1e0346e389b17c23200270a64aa4e1ead98c61695d917de7d5b00491c9b0f12f20a01d6d622edf3de026a4db4e4526225debb93c1237934d71c7340bb5916158cbdafe9ac3225476b6ab57a12357db3abbad7a26c6e66290e44034fb08a20a8d0ec264f309994d2810c49cfba6989d7abb095897459f5425adb48aba07c5fb3c83c0" + }, + { + "input_len": 16384, + "hash": "f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde49d764c270176e53e97bdffa58d549073f2c660be0e81293767ed4e4929f9ad34bbb39a529334c57c4a381ffd2a6d4bfdbf1482651b172aa883cc13408fa67758a3e47503f93f87720a3177325f7823251b85275f64636a8f1d599c2e49722f42e93893", + "keyed_hash": "9e9fc4eb7cf081ea7c47d1807790ed211bfec56aa25bb7037784c13c4b707b0df9e601b101e4cf63a404dfe50f2e1865bb12edc8fca166579ce0c70dba5a5c0fc960ad6f3772183416a00bd29d4c6e651ea7620bb100c9449858bf14e1ddc9ecd35725581ca5b9160de04060045993d972571c3e8f71e9d0496bfa744656861b169d65", + "derive_key": "160e18b5878cd0df1c3af85eb25a0db5344d43a6fbd7a8ef4ed98d0714c3f7e160dc0b1f09caa35f2f417b9ef309dfe5ebd67f4c9507995a531374d099cf8ae317542e885ec6f589378864d3ea98716b3bbb65ef4ab5e0ab5bb298a501f19a41ec19af84a5e6b428ecd813b1a47ed91c9657c3fba11c406bc316768b58f6802c9e9b57" + }, + { + "input_len": 31744, + "hash": "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47860cc51f2b0c28a7b77304bd55fe73af663c02d3f52ea053ba43431ca5bab7bfea2f5e9d7121770d88f70ae9649ea713087d1914f7f312147e247f87eb2d4ffef0ac978bf7b6579d57d533355aa20b8b77b13fd09748728a5cc327a8ec470f4013226f", + "keyed_hash": "efa53b389ab67c593dba624d898d0f7353ab99e4ac9d42302ee64cbf9939a4193a7258db2d9cd32a7a3ecfce46144114b15c2fcb68a618a976bd74515d47be08b628be420b5e830fade7c080e351a076fbc38641ad80c736c8a18fe3c66ce12f95c61c2462a9770d60d0f77115bbcd3782b593016a4e728d4c06cee4505cb0c08a42ec", + "derive_key": "39772aef80e0ebe60596361e45b061e8f417429d529171b6764468c22928e28e9759adeb797a3fbf771b1bcea30150a020e317982bf0d6e7d14dd9f064bc11025c25f31e81bd78a921db0174f03dd481d30e93fd8e90f8b2fee209f849f2d2a52f31719a490fb0ba7aea1e09814ee912eba111a9fde9d5c274185f7bae8ba85d300a2b" + }, + { + "input_len": 102400, + "hash": "bc3e3d41a1146b069abffad3c0d44860cf664390afce4d9661f7902e7943e085e01c59dab908c04c3342b816941a26d69c2605ebee5ec5291cc55e15b76146e6745f0601156c3596cb75065a9c57f35585a52e1ac70f69131c23d611ce11ee4ab1ec2c009012d236648e77be9295dd0426f29b764d65de58eb7d01dd42248204f45f8e", + "keyed_hash": "1c35d1a5811083fd7119f5d5d1ba027b4d01c0c6c49fb6ff2cf75393ea5db4a7f9dbdd3e1d81dcbca3ba241bb18760f207710b751846faaeb9dff8262710999a59b2aa1aca298a032d94eacfadf1aa192418eb54808db23b56e34213266aa08499a16b354f018fc4967d05f8b9d2ad87a7278337be9693fc638a3bfdbe314574ee6fc4", + "derive_key": "4652cff7a3f385a6103b5c260fc1593e13c778dbe608efb092fe7ee69df6e9c6d83a3e041bc3a48df2879f4a0a3ed40e7c961c73eff740f3117a0504c2dff4786d44fb17f1549eb0ba585e40ec29bf7732f0b7e286ff8acddc4cb1e23b87ff5d824a986458dcc6a04ac83969b80637562953df51ed1a7e90a7926924d2763778be8560" + } + ], + "random": [ + {"seed": 1, "len": 0, "xof": 16, "key": "4e2873a644ab37671ab25c9962c09661f89625981a15d94c02b696cb27ab6110", "ctx": "lambda-vm oracle review ctx 0/16", "hash": "af1349b9f5f9a1a6a0404dea36dcc949", "keyed": "e2060fc733e3b0c2b258652b301a876b", "derive": "18592c7bb4d1ffeaa4c1a65c9033c1ed"}, + {"seed": 2, "len": 0, "xof": 32, "key": "51f2f6c1699b22240a722f58d8f4e1d847dab43424ab9c7f6cb007e12d4f5575", "ctx": "lambda-vm oracle review ctx 0/32", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", "keyed": "f53925740cc09cd9a37b1d00920479d275d847e0279c49a1b65001943158da28", "derive": "4624d0bf48875f0faa7f0a14e97b564152d1315d6f3777ff5c8bc4fa7df5b51e"}, + {"seed": 3, "len": 0, "xof": 64, "key": "772fa66e71671508c8633f754cdc205bc7ab06a0b8b0b056d1cb550195ec194b", "ctx": "lambda-vm oracle review ctx 0/64", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a", "keyed": "69d6a0893237ef913262d2dc8e2c71f8d6e8346c3745e7f622898791993de30b227fd4e9500b07b0e42b0f7978f7bc668cf0053f27d993bd3b85dd0567fe3549", "derive": "61aa6ed699d26c31cc82858c9df151de16f5459e752b62d1829cdf00dfb555a4c4240102469bc7e1076871b76c1276734dda1f5aa7147b7c7d8613f1cb763001"}, + {"seed": 4, "len": 0, "xof": 131, "key": "0876f38380a77686e29a0c55b4c16d1d07ae429f299d141e3245cc47f35b9d6f", "ctx": "lambda-vm oracle review ctx 0/131", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d", "keyed": "a5a703baaa710a23e4386586538ff90cffba019e51751a27d98a69e15c372232737faadb85d92412e653759d94aa5abd818b75a9b5108a252ccfdd8b71d92cb9e96b3f52777a355f36d69bc2c495975f889ca448bcf3e099b7940d7fe67038bf2004c04bcd164664285bd69852fbbca42ec60b44adcec2e4ddaea9cb1506320a577b9a", "derive": "a82dfed98f8522164b49badbafa145d39e734f9e868a3c0815b79df69affa65ba9ba052ac0d15b4021676f99dbfb8990a568b77c2c19cbfb5e6fefa1659c08f856b60f8311c676107d56120f9693ee995002dcc4b5e90c4bfdfe7ecbef71e0c40422c015ed6fe5e57432dc6b8c85ffa34b45e43bf94d075042041e47e963e235ea407e"}, + {"seed": 5, "len": 0, "xof": 200, "key": "9d0ff0dd6e45b03425a5b93d7f2f78b817bb8647ea6dfd057fc219ea00d1e2fe", "ctx": "lambda-vm oracle review ctx 0/200", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d30f8a03e49ee25d2ea3cd48a568957b378a65af65fc35fb3e9e12b81ca2d82cdee16c68908a6772f827564336933c89e6908b2f9c7d1811c0eb795cbd5898fe6f5e8af7633", "keyed": "58265ba18bfcdfa1863b0adc953d516e4a3cec2e2d09c4de65fff6d5ee7cb0b9bbf98267b3435d95521c20397e729b3c0094e9c3bfde552931c3451764b6574c7bd0d058b9cecd209a54df2da39a6160729b8e7df79395d5b48204055973b27f5f1ba83971fde2fdad3e6bbe2729eb3e76859edda23f577ba277546934bfe03e3351349cdcd05c71b04677852ab7cd0992c5faed20eb878fbfd374f5328a92bc8736dce061f787f0a9f435d8d9eab7e1ee5245a2e98a556917d2fa2b996b7ff267965f883cdc73de", "derive": "c49da0d52d5230577d675dd17125b7c9d12ee8ecd2dd6b5c7a3bb87e31249b0817a489137718c47e36f1f55fde71db1f47ea3a16c0e53d7e8f61778a199b664d88d598dc0464883110d1898a53f32c4e72728f774bdf5b949c984d645fc48cffd6fc8b867b36b8c568d91cadc0207aff6677ee79f0ca46b34aba8fbbb4d7caead47c39c50468d51c10a9e2d0a21f95360fdd067f234395e488b3a8110faa3c2d791c3bbb836c820a1f27bf8add1994ba93895fee5766cbd6e6524308800bd6763a18ce98b7716faf"}, + {"seed": 6, "len": 1, "xof": 16, "key": "317c727c3588b8406115f201e6afb5cd7abdc51490aaec9c29278c93d48ab42e", "ctx": "lambda-vm oracle review ctx 1/16", "hash": "f633b185e996eb1973f7b5e81da3add1", "keyed": "2cf20a61154aa7b99ec1e808d64504d3", "derive": "031fac35c559618b2d8df9ba8329cbb5"}, + {"seed": 7, "len": 1, "xof": 32, "key": "c515bba50da1bdc09d42110712b6094ae75f9ad50bc151f1efa27b7bf6a4483b", "ctx": "lambda-vm oracle review ctx 1/32", "hash": "ad821f67cec320b5cabc58516eb59f00ffc47e0a23db617879a2817b7560d081", "keyed": "5c20577f80e9d2525dbf76f370eae9b6f2e040990b06323106ceb3f4c59ca77f", "derive": "ab7326c844abcfd063239f0b96a23b1adec9eb82b0a0a5bff421bdc0567a8813"}, + {"seed": 8, "len": 1, "xof": 64, "key": "c54c0ceab664949da493637296c6e82ccb0cbcd3c0fab16029882933ca84927c", "ctx": "lambda-vm oracle review ctx 1/64", "hash": "ad821f67cec320b5cabc58516eb59f00ffc47e0a23db617879a2817b7560d08140f3fdb8e19afa84cf36653dab48871eee1ae3548a75bff74a0f7268bca72569", "keyed": "4c4897a7500db1cd436be752b3186744701743822d6e4926f7cf662b56749f49d2c4569660f7ca12bbfa1483e42df23f21c90d93fd563010e196bf2f19f01871", "derive": "e7696f2d701338d5725eea71e792aef3fe724ee2e257feedb59123334f8dfaa07d57460273286218154b7722ee7d6a6b2efe055ec942df34eabb325afe39bb42"}, + {"seed": 9, "len": 1, "xof": 131, "key": "eb899ae51ef9827c78b6d1b224ac020a475fcd4387a2dc0afb5ce436c256acbe", "ctx": "lambda-vm oracle review ctx 1/131", "hash": "2ca3a1b761c6412251d07b8a707fb85e306530bc393bf47664172bf5078e278177f5fa73496da85a1f43bcbab552dd7bd29627ef86ef7aa1af9499a15a1bf681c3a273bb00421538827f685d575e2f6040047674fd005d3c3a90e83b34a72f8c12b528c1c7ae262c79b5f9e51c66344eddd3b0f5a1387233bd10247c302f6de304d598", "keyed": "f80116f4d31674dd65a0cca63b2ad7ce5e07c25ab4936eab9532422760778ec7bd59719681aabdddb89e23855cb7c8b28536dd7776d22cfb840531734b1aba68aa70b2ef0e485e729618b12fdf9354081a1dcfeb29ed6e65daf5cc9e46e68ac025c7b4726ddaeebcd3aa8c3ef22190322d350729210c51e5bda090c9c2bc985aac4d04", "derive": "691c85c36007c7270f0608ede75a83818afae06b550aec1698ea81935db7657a35577fb9bd3121aab6c952a0367a3603d8e79072c07ed3796cb0a2e926677a06757d7690d3f66ca7ea5b805abba98c17646c6fe6f1894d4bd0f4519c5c7cf8a9cf2cf6672041432d20da197c01169972d47bd8e3f8bdb34e33d2fe89369964d5aee5ae"}, + {"seed": 10, "len": 1, "xof": 200, "key": "119a6021250e721aa23d6096a7e6f8e9e9de5cc085ec7540afbb588f84476632", "ctx": "lambda-vm oracle review ctx 1/200", "hash": "f17067d6d29eb1bb0208ce84d40ee47504c8656b67e7b663eb1d7182e090b0f4399fdbd2be2d9455eac4e40814d8e5b6ed8fd9090ba9c067a17c959a604ae5501bded22884e9617c71ea944374ce143bb040d8cf377b9a09046e9c769710e755940120d137f08a598f4eed9f69f3961ee13b1f25a661dabde0c6101b3cc7b492a72a9e83f4fb44adc30a86f8124b351dc18d9b7c0c711d3a49ca2b5eee3a341e45970d9981692df5b6b65103f9bc270c1733d30d815ca09b8cf134aef4c8bced5da99519932fdb51", "keyed": "2b3b662e366d5b27c2abfdac897acca87a74bfb3eaa3c44a23bec82ad6c923b80f799d1385fa77cb270689f7a9fde16d6f335b76dc4b16b5457377736ee78c26bd0cd9bbab04f8513bbc98709ab3bf5641d4f6de572607748ddde857df6532cafa4075aae6e0afd5940c62af20e71f78eb8aa4eaa25386cb2aa6a6e619fca07226715e4001230dce1ca4c74365273a2248fbb65decd69337fae6578474bb3a0d04e347f930b5ee431c64ed9b528143becf0e6da9c78a3afba6183a08d85877a6a64a79055738c96d", "derive": "566ebbc1e00fd71b6f35665b60075c8238bf87dd3bb4f58a0b7e94b6599a37a983f27b9274b35c9187160e010a70cd2b3562ddc6b779b1d3ff177114e8a22c827649fcbb26db2a6feddd5fc135c057c3c9e4ab63bb04d57bfd89c3ddec14b7a85cb3b12951ff8c5a0ab7bc983854787bad61ebba20e33f85f79cf8f49c0fada60b62c412e8cae0c2f75e374f24c5a41f274ce041a7379881517c69a72dd631a94ea72dd82ef1c3721004db70a1685330337a30da3678427d1aaaffe3a440f2f6b6b9a5fe912511b3"}, + {"seed": 11, "len": 2, "xof": 16, "key": "36d7a2702afdc652e3931fffedc5e1aaea8215f349f940ebd1eb941dd5e47318", "ctx": "lambda-vm oracle review ctx 2/16", "hash": "0274374489ae986e5475921ba458907b", "keyed": "54ff07b6a6acd7a1f6b0961b318bb08c", "derive": "09ad6852b188894de999199fb6fb74a2"}, + {"seed": 12, "len": 2, "xof": 32, "key": "a5d7f0a5d770f63da9dbd45bed6874726ed533458db65685690376ab47d6fa82", "ctx": "lambda-vm oracle review ctx 2/32", "hash": "ae2ed86f46c2a7fe7d0f242aa6aac5996059df9533838c17c786c04b9ca0f644", "keyed": "de176d34225fe68f469150df8f5ba8cd61bc4a03872962e312b8dda7e83669e2", "derive": "30dfa512613e64ea073c79b3bfde819c269dee858a1af35a9ebe53540098c6c4"}, + {"seed": 13, "len": 2, "xof": 64, "key": "39705c1c83976fb01c0ac14b1f5c5170b5ed292906fe65e483ea6410ac670871", "ctx": "lambda-vm oracle review ctx 2/64", "hash": "c91ff7a53e49408b407f10d8f9c8ed080b5de4811502f0d5f9c5096fb57df4f01b5b2fcda59f9150970abbd8e5bf0f30b5658b5d236b66a70e26a566f77a88c1", "keyed": "5a61e26a1347ee4e3712cdeef297b4a435a896cab11ca069195291f401412a26dbaaf4e03fd66a258bd656f094b4deb643342edf6eb2d237cc5e5c780acdea9a", "derive": "2e6195ec1ba4d7d8f198e6d5e55690bacb567b00bb3c0fee39103463f4353e7b85fd3d70172158b7053da07500906d6d5c3e74e661c46d364f096064bda855f3"}, + {"seed": 14, "len": 2, "xof": 131, "key": "f024acdc8f5356491b8fad84636e34b608fddaac3f4b5c0c67580d0ddd2766a8", "ctx": "lambda-vm oracle review ctx 2/131", "hash": "f997bcf0a91417c7a4a7eedf735d986be1b183d455057a5655fd77147c0db0396da06133facf9dad5523ccb852b43e531f415c4bf0aa170fa9407b1e6667d4e0e3bb59ab2ffabbedcf060af9bda300dcc6fcad1b223c6717b574a1b512777193a9ea7684ee725fa6a93210b782b128d2aecce569918501e61ccc664487174dbf6c390c", "keyed": "0a4090e3375db9ad9dcb59055a53cee9a40b48a7c6f85cb6de6e4b77d626e2540bf035a7e9363b04e0595d82107d0ad723686b76de610d2b32d7155e1874af4763a917b61a47067b0c25461f9a4896fcb1c660ead2410858b7a29aa4ccfdaf49ed28f505085121acf3d75cba353be74a598a8e81560f9e8e7174ed6c84a3f623ff3dbd", "derive": "0a8f4b6d80548fba6628b0f9342e27fee4be72a5498b6fecb46ac4a45b5e320cf94ddaf8c7a4131b6e5b46183cf08dd2ae5310f75dace83b9c5af6d7ce7041e01f40ed2afaf4a38e79409cb8203a5d75301d40a99a45f3f0289757874ef5df2a23ba941b3ba23bd70047c5148bdd45ef502e598a8c3ca0390756fd332cea76d1ccd0da"}, + {"seed": 15, "len": 2, "xof": 200, "key": "85bd87a61d3b05fcfd17c2258ad1a40b37396d6aa816e1127500d5d5928a0a90", "ctx": "lambda-vm oracle review ctx 2/200", "hash": "2c9c567961e877f29cf5469d28f8b1341295df42fb56e5e2c3c240bd63c57778f4bdcde180ae4be83337936fd51f01349e5356b15319852c5b50dffd1c099c34090283b352e8d350263103684047399418cc319d770105f23ca00634cc733f06e320db36d68954c6d44fc6b5853462ea1019a9941fb1041cfc00d590c1c4e487569ad44acac818b9b3ee7d406c736f41f70f72cee3b133b2241a3fb8627a0fe3fa92341a6b6fc5e1222aa688f49d936b6b2ec99cfd1fde1fe6611fdccdce74efaf54a9cabe220723", "keyed": "024283d0b8224be9032765f2cd1076dfc7f4cf6cb947a9c7367ffb4e082cdf137522dad8ec833a35c211797431e9de7d7ddd3fc35692b7cc943400095e978db894b9bc37e1ca7ed271d1edfc0befa95a64a39e71c34b46d4d557e3770f8f01b7c8fa7349ffec6e593e10af3d7bd713c8faef0b00272b347a7baa2a135f006c5f3efdca2ab53b73532f30eb377ee52f9833a556cf76a90e51da5c5e9b42666e09788d4308103f19d596fd961255801a65ce10c85735830e039830ffc36da06ef54eb17dbf3d99e3c2", "derive": "600c0981573e4afba64240a4246475ee49f8b73fd8c0f89a122077676c63e59ec6633c5ce0b851122fb91bbee915b6c929ec653b651986c0a5f9b75b421f7e2813c46f885bf203ff25e511cbaa904d96feb55c336ca224ba11b1bac173072dc69fe3e90fab5bf5b11819479652c5afe014a2e42406191ee4c456a03f7ff7c06ff77c4c9f9569592500ac9f1a5e1491080c163e76e9f4ecd849f6e2818e97db2cf67c7d616d801717002173f4f331da718840597c7b381d1e164a789169ac6c26ea34a470f8692bfa"}, + {"seed": 16, "len": 31, "xof": 16, "key": "a7f5e028fe27073413baf3c095354912dfd380d484e9cfeb548c1492fdc5bbf6", "ctx": "lambda-vm oracle review ctx 31/16", "hash": "e45042ca397670d5d2ef5eb5be0783fa", "keyed": "a5a9e20141df86780d1b27f11b0d1bd4", "derive": "a3bcef201e7ab8174a69e1a97ea66de5"}, + {"seed": 17, "len": 31, "xof": 32, "key": "cda155913dc188c34335e58f73091fe30cce6cf755a62f967c0837a51ddfb283", "ctx": "lambda-vm oracle review ctx 31/32", "hash": "b014f91e00ef5410cfef91b1bd8efd88d398ea2ceaa8a6522ce4157d6d57a5fb", "keyed": "61bad733c76597cf015c4bf9bf6bc766f06369fd79a6b42232946be96b84f719", "derive": "56ca83cbed45c5e4cf5925a22493de95b252ece7d578655be136a03985e2cc96"}, + {"seed": 18, "len": 31, "xof": 64, "key": "d0fca40ee0a6fbbb65f2e91e27643c0d4141d20b6aae333f64ba2fc4661360cd", "ctx": "lambda-vm oracle review ctx 31/64", "hash": "3a9668075cf2350c98e361bf3138f6de927f0c618974a38df8787c440a10fc6e6839bd4613297bf68e1f591f24087f4841a50e79309f96b17b132dd0e1ad627f", "keyed": "b6bde96e47e786800e0f501a77babd92291666f168d05a23cbe57cc7f6384d4e58ec2199f0bc02cb719632403663483a409986f48caef7f1692d5e32668b52e0", "derive": "01e6cfde455c562b5d41ba800be896c0bde376fa3803ce37bce821b92bbfc7e1a08b375bb84eeb5a2c3300dd667ee9212d441cd382b73b97fe0b2172aaf3499a"}, + {"seed": 19, "len": 31, "xof": 131, "key": "f6a765475e789f2922e039484a8f8b1d9da6328f67701ec6ae99c800b5f72b61", "ctx": "lambda-vm oracle review ctx 31/131", "hash": "c42704dfc6c9013843886334233782451a5f5fbb8fccbb8e1b6ce89f3c2c8d82674dc0555a7313e23254a13c195da6c8c36d5adff3ff1da19a739a78efbf324b9d32017bfeb744676230118e5f7714cf22b5ad0eb494bf3b60f67e788b11cb9cba815c3a79a60314ddc7c8be71d6febace72a99b1bb6524446fe6e24735335dfd4de41", "keyed": "ebc1ca503e3ff98fe60319c9464e35b592b1169515178d01a7a4a09b9cdf58d041cd589b7d91438b01cf16453b03917b9ff4f33a9027c6e0fda2d37f526942120908541d46f232981f2927e26bcd9a6a60382ad2f09ffc581331910c02176428c85439817e96262eba404a76705e60cb689c9676779b4945bd25a485f7332951a956a6", "derive": "651fe4214293835fc5aa7353337d3dbee3f9f8f674271c403d41e4f22168a427cb60094adaec81f5f811d54c7facf79d23b7c8090c364d6f25da9263d57b053422c7f6d4524aaeb1e064b5a13ecb76df59e74a28c3af83980717f9c50f4ac1969b57e6d397d3b4f0e36acb2221d3d82779866b9830dbfa0c98968519f5d8ee9f2e7d21"}, + {"seed": 20, "len": 31, "xof": 200, "key": "427d2be2a0a273bcbd83a71c6356ae4db1e6dae70d4ceb93f8d3ceb5b3c2b0f4", "ctx": "lambda-vm oracle review ctx 31/200", "hash": "f2c450b25acf5d07c0eca77f91565db7b1724e825cec605ffa4c3811108a19ce7ab9bedcad7e970e2bdb188f807fcc8d176d7af48528a57f7f7e97b4cf480db33418b8faa8e9e0ec76fa02adafb7751e5ebec1d9a088e7234b3976ca652b2a58fb50402ad4db627b4a3f087295ecb180d75a2ea2cbb99aca5ff1cb01ae0faed7a17692f03d13454834f14247437a9a00749b68b17c633ceacbbf1ed72edea9321d3e3edb41a76acf21dc406224524a1ede3bd7f535bbce38fdf3429798a341245eea76c3d9451fe2", "keyed": "0279a7640de3adf0825a3840dd78c65a80162306b133ee52bc207b2b7bf639959a6fc394a2e23e79ce0c7cc44569aad0d3d24b3c39c010d5839744a86fe1f47a1b55e4dcd2898d13aecc7e9223010097e5ccb05cbaeb2b258c8785fb1d2edbdbbb827c9c5e2592ad901028a5cb5cfda8215acd4f36c03088f8c0a976f410991cc499ea5da7587c671a17d200949e6e801a6f1e8ba9e9bc6e704313f9f577b5a649f94c75ab983708388e232cbaba93097fba3378046efec08279025967db8540dd0470d24c5eee3a", "derive": "03a483217460e212f6c9454f0d3a0e0b0ca69d4e0bc53ad07724d66565598b82cc6bbaf8456c3d9ff6eb092ab38ba6308c82717daefe30ee3a963a531e6006f550d72bdf9335fd81e98dc5ba2e694b34b4a5db8f13f015f9c0a0f7bca527e2e164635dd3503026b2060d6d1ee44f887d5c3debbab2189b285585801d85a5af8b0374e58bf839ef6eed1c6a912d57983ebd5292af99b15f1e4f08a5c8cce02b625e729f5a5a1130a1f78de0bc3da9860349f4825963906b01c49ed7fed54bee7ed8af418d109d6acd"}, + {"seed": 21, "len": 32, "xof": 16, "key": "d6a839c8611a7bd19c9791970aa61e9453a6ce5014f54c84af38377d711ce1d7", "ctx": "lambda-vm oracle review ctx 32/16", "hash": "1f8db1bfdd8dd100a84848f8824b888f", "keyed": "0bcb7116c7635074d6df75bf5d5c932f", "derive": "c3191d21509721b6885d7498e86005c7"}, + {"seed": 22, "len": 32, "xof": 32, "key": "6b8488c931ef0c80a7a15aa039a2a4da72e5fd97686f344c037c08c151a7ee29", "ctx": "lambda-vm oracle review ctx 32/32", "hash": "39eb5d3bf52d1c4855e9e7475cb795c9eda725f55f8e77cc5e97decb3a0bb2b7", "keyed": "cc5dfd72264e95db8a511bc890e2339bfe3c8330b368fbc5208b33f072a9e0a1", "derive": "659185a1756867f59590deba74d025854eb6b6d3d2a61ee8340ea9efea42cb09"}, + {"seed": 23, "len": 32, "xof": 64, "key": "ffae277d623842753e96b906f13377ec53f8218c863a7ffe52a5805ac20c9468", "ctx": "lambda-vm oracle review ctx 32/64", "hash": "ddf820aa0395b67040adf9de32bfa880bd7bd6a63776c5834309436e331523aa3bbab766ae85510377fce8259d0b5037bae8954b037f0faf9136d5858053f1af", "keyed": "108fb30d7a4ce089e673a206df2ba91600b992d5ba40f04fdbc0a42b99e6e1dfced81ddccb58738b92037a6903d2d669c9a8bb8880e45302f38824fc7aa3177d", "derive": "b14a08b72587ef6f8e95ad7c39c02ad3f635da206d80dae709fe6a215f0f1a5e6d897e6a0d6666a380bc4896dcc9eba9f711845f921c5a745eefcb16a88b9566"}, + {"seed": 24, "len": 32, "xof": 131, "key": "445637047e96984af1f65f8d683deeb28858305868dcd5da010644a281f75396", "ctx": "lambda-vm oracle review ctx 32/131", "hash": "04153ec8aae42ce4e967b24e4f0fcea47b0b5bc6ff27ce555d0e29ad1afb044681191753eb69bf3c246d04bb83f0c5fa6c2b169cc1f84188a72c52e28b86c953f9bcfc0428c49c4a9ef3ac2d6d9f372ba83f8fa4bbcf568b38c05d4e0baf9f8cc5a1b915b8f86f1eb8baf428b79bf4eb24eda5831a105e0cd7b9af8954d5342b4d259c", "keyed": "b78d7c2f73e8bb10ae2d6ba2af88034e071497f362978f2c6dfb41038c2ae7cc8466a2e599237145b1117907fc23c9f74b3f9d7cc81e8c75cf849d31c24d88e256feb79810ca0c90a07c066b95464a0ca3acf5848ad903883c4cbc31c033f6e71b0ea152433769feb322503af0700be9833e8b8101d4ab0bd7302880ff4df965e442c5", "derive": "33a9f9cb60dae21962da63ee1d6926ce0bf1292e6f03760396d5b292c295843b13ff19472fe964caddbe09559f9840d1609ad0a3be1ba5a4b6c0c4688585ecb1a399beef3d4bca758fd94f96e0aba1c86d815fb5ea72a7d03a29b7abc2459a77aaaf8db735dc80abade3181de8bd9ad2a0f4b7d155b49e8300666252337b4aa37c975f"}, + {"seed": 25, "len": 32, "xof": 200, "key": "6a021b8bc4caeed9bf9bdca81c38ab88786e351821c08d22314b361187bc7318", "ctx": "lambda-vm oracle review ctx 32/200", "hash": "e04c6f90e7c7c64dc4482711d6fc69dfd0311807c5de185abdc58bff4e5dc75c950ca3397b856cec6a773e31ea95470bf2f8d16072142c35104d90537e6644d191387ee0250d617def6ac698c20324cb1a7ad4ee0ca9a72a0be13478341383041ef3855c6871ca02f92c0a30ef98748200d1ff5c483f28068325d80880b37ec3050587fb66b3bd40cd84c701c163b0fd33a22a26c7fddf7833e7012f0d282613e263ffe5f93ba2997829ad0b6583e8973cd656e531ffc34418bb5c454ef1571a8d719ce4aa9dcbdc", "keyed": "3ee76706e52a25d0b11fda2008594cb7c0d108e0a30a799685e68a36fc4d576a872dae2c86182c06f744cb84088296f26397726455a9cd3076027851f94cb2db376daa3f2fb478f3df351487f0d55bb9b789f3481496894af5f7295e76c9144bd6db381d89aa984f0d0f1fee767c61899809979057129f8395ed90a6eaf53b910629dd38026013769f2a0761681449cbe68e03e7e87201bf02b68f1f89397bab16da8b38dbd50278618fdc4dae10946246fa6cca05264bd6f4333921e2fbeca60bba45240b2e248a", "derive": "f8ce88a2f6f80dacafd922474d734c038d7eb0e6bf14a82055d64f14343336c88bebf482471aa7d0b4119d6bbe762b4d9f6284ceb83acb215e575f14e0ec4331b1e5337f0b78f5aeb683830b8337df041c0270aea9d7afb7fc04875a0ec985e16a7b2a60473a3241a7aa404a713b86061d8f87d20135a29294ff9cf4137175064834410219d6d8cfa009f9f866d4560488eb5933925cd15a4461b25a9a2f002c02a39c2e3a7c0b0517ffca563c1398aafb02c7a786376321a38d09b8ae68491b5f2d1c43db26d2a9"}, + {"seed": 26, "len": 33, "xof": 16, "key": "90a4ae28a4be801aad16678441130895919436864549da4392404ce5abe7f28b", "ctx": "lambda-vm oracle review ctx 33/16", "hash": "ef56ebde8209e7f40467b2a34930f5d5", "keyed": "3cf2f08202f6d76e0ca02fdbf2cc3f19", "derive": "0fc74c6095da0ff7afbddb487f526fa5"}, + {"seed": 27, "len": 33, "xof": 32, "key": "b54f00030d216c7e1db895eecafc89352606995fd98cbbb88426abdea6c0e38f", "ctx": "lambda-vm oracle review ctx 33/32", "hash": "61472133b26ffd46ed8a0321fc278559d720a5ed6c43a55bd771bf08aacf247a", "keyed": "3df5ec7583584487ab47307fc118aef9bbcc697193ca78ce4d010a1537359e66", "derive": "346249306132497649f765a746576fec3c8640d5648f6c821e01c94d6635a0ed"}, + {"seed": 28, "len": 33, "xof": 64, "key": "dfdeb3bedd683ea5e7be357f352108909e305927738447cd1e3b12868f09c861", "ctx": "lambda-vm oracle review ctx 33/64", "hash": "9d2b24db2bc6fe675948d163f6d44d6575581aa479afe1c1e831044c1af8872a48a68959d7b6c82f8b8593a69af6270c73732fa96b5c53a83a5f3e73a0c7d239", "keyed": "350e2f86f87fae4ec59a64492eccd9da82d59a3e995b4aa5adcbbc66c1569fdde120c789a3a7a52f41fde3707ba3a06cb4a1d35af80ed299e9b3fccf538ea0a1", "derive": "568cbc5904963dafb7b00e79915b0b684b153b3a4a53d11067a7f01ef6b84676f75ca7844f50dd15ce9c60cac2e7652828df27ba9a671545b53ce885548f41e5"}, + {"seed": 29, "len": 33, "xof": 131, "key": "73092fc14a279f4086511f0fb033b375c241f8786cac69a76b9e7741e23ab604", "ctx": "lambda-vm oracle review ctx 33/131", "hash": "bb438da5ee6ee50a61e2c984008a4b816e327c95f6a47ac8c11d7ee537c0706eb77612e0f2dad149286b098f892043a62cd91c9fb3856556d7334e1a5de3ea36fd3df32cde9e7d68e3efcf38b4a0acecce095c698d464765526d39146e66a6f2382f72e0b2db791f43844de7975cd448d1183a9aa6c5346941a5059349e372dd7ea593", "keyed": "1adb706a90fcced156afccbacda27db9e57ff84b19b4f73cc72cdcfbe9221b01832e8ef79ef9829f9fd922eeb4584ae68d23d61d19516048748e3414a9d0c495fdf513523a881fad9f36703fc9d6eacd0e36aa3d79deaa64bc6ec476fc5364b70ccb188b1400f77ae4038cfe490cae20f58486f33a6c0e0adec5be45f037631ea24c08", "derive": "08bd3f3f80c8cc8d61c67f361948a703b3c648b00df9543c02e1d894a8006707a204e9605106f2ec362e4e5a230ae89267ff2708aa45d04be8f4e5be36b539c3138bc847f827671d46aa4321c5dfbeeab4ec376687bc91f9df331dae0a187ed31a88b733eb2176438d773a111158ae75561f32beda6cf7cd923dccd14350addc44f4f4"}, + {"seed": 30, "len": 33, "xof": 200, "key": "2a2c4ce3bb5f2815a142a9155a0092a92a965f8cb20c2100498799b76422b677", "ctx": "lambda-vm oracle review ctx 33/200", "hash": "dac08212ee797b2e713b25a596642be078022fbcd06859a4ceb0990e105f971581078d79719fce9ab8baecf7d44f7742c5e7faeacc67795455549120cf86d2599af82d19d76b4df7c7a9332eef0589a3ea71ff56f7d762adb57617854ed8340b59e5ce7716f61f7e16fef7db370a1ca355df79948ab0635bc33c8df93d72706344cae8a9d94cc17b35b869aa6bc28257e871f336ca931f68b5ca2594af9ad1b4518e3028422660db19d48a810cc0c837f1785e0d94091c847799c732a9b5a9effb41823c6f32030c", "keyed": "888a4c62b8c06353dbd9889296c692675f44f803a5465d1395109e0f8a07f2b2190490613894aa47ef30b3f545daaad007df969935d3bec5643320f3660ad959160382b47c1559f4cc5351ea29fcbbad62320644a1546343f7c1717418207b239abd8834f453ffa07b12c743f4d697dc8c8ef10eef74967c50a6d425afd387b1f13cc97f53774810547bfc3f8377a76a75fa7df2267e19d62dde7e5b84a6f64a6df693da3db93f2c20debb4763341e6e70a79cd290cfc5393336d4c18ae70f3cc11263eed615d07f", "derive": "4665c81a65f084fc050577c83edcc924438c5ae94efcf08833de53feb697ddbe43a6114cb825a7962455ac7834ea85add26679b75e80abe73b260e3db1f604cc570f918bf97eb9106d029033d92bcfd8bb800d160b9aeed608c94a098837cd09a8f04cb5c9a9820bb100740ae5f41023831a0fdfc40c7676b3112cb8865911211efb9f39c3f8abd15d6c255b694c94e151f922750acd1957b5ed83de0b8ddd28f685a14855174ceaca79094fbe6f76be6c61c9f7f28d582bee692c246f325cd16b7ec854f8583e79"}, + {"seed": 31, "len": 63, "xof": 16, "key": "bf567d39b3e6ef07817fe823e62a01cd76885b59b1ca71618f1145521868d4f0", "ctx": "lambda-vm oracle review ctx 63/16", "hash": "4262e019e820ef30e1e25fdad359c3ea", "keyed": "9f6b2487c3fd74c2a9d9be0e2581fe2d", "derive": "caa2126e0420650fee91fffaf7ede68e"}, + {"seed": 32, "len": 63, "xof": 32, "key": "fc4dd8ec37ce39f879d8e00a0c73114a45e91730dc70d90d18e5e654c0538431", "ctx": "lambda-vm oracle review ctx 63/32", "hash": "aaa39d28da9cb6b404bcb7d32a2920ba27738aef1c3483a6cae69684bff16889", "keyed": "7f999b8c7266cd4d41d379e431090084c2cb66cfc23b94b90de8aec7e08d04d0", "derive": "1d27ac79f3f877ffac23c9398d7c82514b41939ac6ed4b5eeb04c355a0a03802"}, + {"seed": 33, "len": 63, "xof": 64, "key": "228af733f9098e51b260f6f3273f85c2c90dfb84ef8e22d8a34b97c534a28a21", "ctx": "lambda-vm oracle review ctx 63/64", "hash": "d05537d45f4d3f83c2a99116e2cf7be2360a0b25ada3f5f5df23ea147ee2dbeb3839bdcdcfd838a0a8515e0648af60d51c422b7acbce9a9e6b88b1783b307fc0", "keyed": "7c5849505cf5500ff148af37164943ba45fd7d471f23e7b81d8cfdbb75ad5e560fe1e35b063ede7d7ee23ab2b82b3ac68495e05623b3ccb8b3497a5bd7cd11f3", "derive": "33d52e8cfa97f0fa1178a5517f8bfdf44b77ba41488613124d3061e601a1f09d39bab70e5a1fd7bf50d753f58d52773ec61a1969e103265b4a900a4fe9c8aa4e"}, + {"seed": 34, "len": 63, "xof": 131, "key": "2576e5cef6878d6ad1d871bd733ff3b19eb40c1ba2382108f942ca47a157dbf0", "ctx": "lambda-vm oracle review ctx 63/131", "hash": "c9e1b5beebbc4b4555f390e7671456ea4e99bd0c609cbc4879cd7993bbba429a1796e0c86ec72ab580ec6cb97624d1c4fbae8188d7e712b666e41f73258a6bff44ab4077ec5caca38a7433be48c486e99c165087a546594e84a04518d46d572f50fc452e62c5123889bd4082085cfa4dece385df7263294220fe2e284046529658fed2", "keyed": "61dc08eddf340491716507f5df1eb74b0864d8a736357041a057a556b5539a69ce82f9b91bace73c49b01d841edc68f1ab8dab66b8d0e87ab8366fba501f7a5c4ae280f7db9f459187b18cec8a753fb500de2b7f6e4a35f7475ee9b71d0f570407106db39e6831e609f03957fc1fcebc8799536498fbd1d713b2671474004cc5474594", "derive": "da1263ffa944e8b880d03b5e8e34ffc10a310abf855c028bd483542e51ef3fb27f50c9bc25a276bc993255b93c1132ba8fe8870e8a8326fab129ced339aa145930e1ff881082ca6b9abcf2dd11849b4fcc17e3d8e871b331f662b088f2d32682f96ee983e913867b78eadb514a5d57eb9a60574b6d5b5c4eb9ec42fee23de04b77b8f2"}, + {"seed": 35, "len": 63, "xof": 200, "key": "4bb395e425b26cc9923fcb8a4b7d3b4780c76f48520d1037b950aa020de21884", "ctx": "lambda-vm oracle review ctx 63/200", "hash": "7f7d40e208d339f811314b5eb5d1bac48af4124805f7a02c78240c0f967e67c29a41d9a649308da8c46487bea1bf6eb8e0f3a38eb7175e68cbd731bace4048adc87502e2708c7579a5d070c5c0c6fdf57164a8a9a8b7b1329882d0bc0208b5bda0717706e61f677ebf648fc890ecd8adc02df65793c0b4500c96695c832d473d97962410b3c4d4e9e4431af5990f22082d6aa4ec841067b90222972e63d8b387ac52e7bc31d5d7861cf4bde69a46ccc376550144c92fe0dc1b02d3355930a017bd1ad14b0690f0cc", "keyed": "1048cb6fbe2f95bd6d2dc387c01d9efb207f63a30adc9b34567e9b7c364380cdd90fddf1cfba6fb406bf7dfd24c4d84c17c562f20459a739218c839e31593d69b4676fba089efd4367b047c10b2f4543a24fd66a9913f4ef84cad9a299382ad63226d507a2f5fd97a3e54792d71bdb2b6ab407f6ccb2675b97e5f56773f81fd776014c88cf04d0a3d0adf2e715a8f6e94876da4dde49189cdad6bdd4a460df60869a03b5a2c27a2375b022293e7f847893874d6d1bc1d037fb812035a4394bc8a7dcd2fd67ee4afb", "derive": "ebb1db90953880b49ed3c1cc8af140b21342c04fef76b6ab6580f7ba7eaaf332d49f38d1ee789b89a5a179e977beb397ee57fc1735a98e5ed7dff73ade7df059c9f3212d358c1132a6db7f12348758c7c1190348f703193590a6495a313c9cd746cdf4a2b41228e4ef0e5e8562e7e31825a9565293a545bda5d536fb5060bef37025fa3f17998c237a01a22c430b6344358b3caf4e47d06c0c382be109b97ab283459bc6fc06c71309dc38f7bff007b78efa679eaf7d34ae4227f2348f408f6205409b2c3942fedb"}, + {"seed": 36, "len": 64, "xof": 16, "key": "dcd8315fa4d93ab7401b877d6faf3d91216ef5be067af7f88203136a6c930cd6", "ctx": "lambda-vm oracle review ctx 64/16", "hash": "d157ddbefa378438f4da2f56ba3999d7", "keyed": "7d9689c610ea2ee1adf08f5fbafda494", "derive": "5aab60d350237580fd2e72c7fc88e25a"}, + {"seed": 37, "len": 64, "xof": 32, "key": "70712f68d2f62b0ca8b89e939e02ea75ce4906752926f53e13e5123e565c2ab8", "ctx": "lambda-vm oracle review ctx 64/32", "hash": "2f9e3ee7adf6cdfe9c2eb4c5258a7a6407a7ca7c6097c31e9016b7a2d3041a6e", "keyed": "811d9602e043f9300dd08eef1b7d80d2636dce0beb1e3d3021375830073e779b", "derive": "779db5d851ab425e380f54d3e3b7dbd35e86163d5791b7a7e6596846d54fb981"}, + {"seed": 38, "len": 64, "xof": 64, "key": "0501a6428cd4abddb3ba9d7c7c7b7233b7de463f9b2bc76271f69c2587bae2f3", "ctx": "lambda-vm oracle review ctx 64/64", "hash": "927bc3149626165538fd524c3bbfc3b9dfc2569807604b16625f7f1a6284d1ef4c8f416a9ab9d7ba0a345a302a823d5b5cb1c8c6a5773d3d3ebfb894013b6800", "keyed": "dde8934e7634a0d90b5ee4875c1cdb9efbed89f6772920f6b79e355385ee69cd431d8038ecbc511073404d93af5e5a7e5458dc7c661f229d509bf012c4c04696", "derive": "7e5cbf68decda9fadec8f78802089a0b5174adff9e5dd810e048361ef1ff2584914c0e5c60d13e25751fde8a581879b4400b857cb9953e9210784cfea0919a63"}, + {"seed": 39, "len": 64, "xof": 131, "key": "999aef198891c1934233fe032a2b70c199be1eaf910d9a67882bab195bb665ce", "ctx": "lambda-vm oracle review ctx 64/131", "hash": "896f9dcadbeea0c1e49fe27f7d1174641cd6e5a9c281c0b953a4eda56e89b0fc4ba78af86bc387aaf7d8de8895fb03e759242a139971db04e2fd40b63ddcd4cffd009e5eaf22a8e39fb3c8d1bc12f05983f4a9a033e7b67fe505b89ab36af9b5ac95874685e6b6b27d78423187d00119aa2cdc3a273d1e1c08067be0452831de1e9608", "keyed": "0e51ec8678880a2633455762a587de426a4872976fd22d77d6e5911e97acd1a880178c3c23cf9d0bd160e0d892dba818d287da315646730c7da310809c43061c38e87f561025c098e716edff0196671d113fd61f7e85b5b6214be0221549619effe0aee19c32ed46c4a2fcb2a8642278ab8d06caa80ab65206cd974b3a2c31ba191859", "derive": "b2c4be9febf61f6cee8db15af3c3db73029ff205dbf16af4254b0a069c5a948ca7374ec85f27821bf9f6d144d1270c5d1e080c811844fb24c702d9f05e9804ff83d04e1decacd935d307cb5214cb1d2c26a177f48c6ae6f651ee87fa73977b70ae50c8035df33d208af5c2a0941c68a1c5461f5b8c39abee9b37a5ec8d15b61017e458"}, + {"seed": 40, "len": 64, "xof": 200, "key": "0fc85f0e2a4062e32fa0ff98397bb7d36232ca22dd0577918026b85492ca3a6f", "ctx": "lambda-vm oracle review ctx 64/200", "hash": "602a68f3084d758d683e9bb1e87f90cf0eb3f5fa9b1c72c67ae7ea73844265307fdd7bd060407314419dfeb50831db7271624dcd8a306666a73e2eb7dfc7e0d802dc0521fa05eb69203d791e90b1e4bea32887225cb5980e2631488f10f0d18a98f09a62030d4dee3f03f58a0f0151d3c533e731df86e4df2bafe9d395df1f7e43e1ecb51c6baf1115452d04c11cadd5d5fd467fdaba98bc04537ad265e45c8a52fea8831c8ebfdf37053576584270f4ceb12d4352e3a4ca386d0c94fae692f94bc264d956fdf6a0", "keyed": "ff0f40bef8b7b7229ae920abfca4a8c95648070075d8c960f1add315ecb9528df208bd76e4755128e09b8db80d5f3264ebfeda150910a1bb9597105681632a6eddf071300520c5260a2be70ea6840e2e9678ff236aab3548e151cb3d50cfe224d9d679c0c63ade61af9128d3ad2832e4370820d896486b3aa7a45edffb2a2a20b9f6d0390a3d0021c81be5a078261d05d3ff2c4b1fda0bfb20db84d70e3e1b3890861415a9f0be072e923e36a8f58e06003746e51cf12bc001ae7d7fd7afd24819c7aa4e7101fe28", "derive": "05dc3dedf0ff11444fc56c14533af0fea2487ec806b6fb375447d898f91d31df614676bac84d24a592f9e720be0f75b878894dd31d2cf91c55e98c029a4b966e3d8bd014d503040fefa66d032d47c4f6f0f49dc8ec3b417c75e2365bb1de95cc3ea68a933a15330ab3b9854d93715cf3761cd67769349c5e2e26f68510e01d7bd5145940aa2fe0fb6bdad0c4a0f3b2644387acaa6a09df2da5658686adb1530620d9fb3fa0f8cfa117993737d84c445c0aad5c3405efa569880c34f5f52a03c4edcba84e1307c607"}, + {"seed": 41, "len": 65, "xof": 16, "key": "3405ed72ee6e1e23b3056a704653ef50f87fe7b96d04fdb34006c9e4cfae0999", "ctx": "lambda-vm oracle review ctx 65/16", "hash": "dfb16d551fbe1c4aa129d94330963e3e", "keyed": "fab08e278a8ce8560dda049029890668", "derive": "b602c3e3951abc54f9cb839e0614a03b"}, + {"seed": 42, "len": 65, "xof": 32, "key": "5af3a92e08b2d165afa1ffaf4b4a718184038d58cc4f9ae4d82c26efd3f990c6", "ctx": "lambda-vm oracle review ctx 65/32", "hash": "4c7dfbf6b14dfd1b1b6cfb4d7cc5fddffffaed6a8b0d22fac9d1c2a23659d356", "keyed": "19edef3cb85a9dd2aa998b98d32f1894962d92f737a5d90b07ec6cf56c57631d", "derive": "f2900904342a7229e5a5291301b60d6e188d591338ce5a69393f0ef72ef608f8"}, + {"seed": 43, "len": 65, "xof": 64, "key": "8030ebe642164919b2ff6dd0813434a78d960adadd27bf31473939e8b852211b", "ctx": "lambda-vm oracle review ctx 65/64", "hash": "e1050d263b6e42425a3943383bb6decbc90f6d7efac5353c2d71bc7db0555783781da185db68be42db253d7db0e473adc4751ced78b31aade695fb6173743825", "keyed": "701bc44c78df866dac167e79d952882c02ed7360755cd86ba79900f49630eba449fea7cff119f08a77086143178fa25b866effd36b4ff7928bd9baa241b04bad", "derive": "f52acc57658d78b7b9b320788eb71fcc3af457c04e12c94068d7805529815ee65af87693a00b873b332f5e9b4084aedbd678245493d2a5d30a9361abfcd96ae3"}, + {"seed": 44, "len": 65, "xof": 131, "key": "ee528981c9463bb1736ed4617b4d514eb78d9e7589f2169a2e9c63de8ec916c0", "ctx": "lambda-vm oracle review ctx 65/131", "hash": "36077359619cc4e2676b8fa3026eb9c6794bdbc76d73209a22276f7ef6724faa9ca61c43e9851dc56752d630469146dc84791fbd82c8213854c08cc4fde6148b19486a004872a9b54f9677ef41f3efcf112d3dbf217e8684b6fb48e002b78292f01bae8e0bdd10411d5953dc17c9a2830d1570a9158e15f76d1b67e371078bd1adcf79", "keyed": "b76a2dde6eebd30fc4844e18e704a7d3a128ffa2b996aa89b8f8d8c57400c67c4bfdd7591266d43d359b9e89ba680b6ab6145bacd51d72c2869982fb6bf7c526b319c5b123dc6233f5664f54922ebddfd1d8d25841c3bf7a12ac4a044292ba069c318fa73d29ec98fb86a9e25b0f369a3b79c3742e12e4fff2f8672d38c8b37c145df2", "derive": "77c702e02e4ff332882554f0cd9310ee7be65596c76e1c77e50b7df84065547f71efce4cea00d8bed8bda6ba8d1740ded9ccc42fe1685c3c89f6624f0800295659d0fc690f4bfb0da9945790b9ca93a44bcd48286400ea9dfa3aa38ba0dc1dee2977a932056cd0ab02f96856a44919079c03359201fdccee8d31881923495e0f3fa163"}, + {"seed": 45, "len": 65, "xof": 200, "key": "83ebf4a74303c8137f44412c714708d00e639b7094a7810fedd9ab0c28165013", "ctx": "lambda-vm oracle review ctx 65/200", "hash": "a5e8ad40fb449a0ca7f50cdf35b8b5f192d84c646c48e1f3db70a8ad6f9de5743b26582ef3095962c2e70cde4205085562fa2003bbd4d6ff05d6e549bdf140cee596c179f3b4c83f06e1900fddac36630d16a89000010a2dffd35f70019fe3d35084e0171731907acab1ada9de117ac51a6bce94131931f4c1d6d2a16fadc000df322010c869c9e957f3d23798a08854abf3214792033766e0e6c01fe14d0061a397e264e4ba36e08e7262db6d20114816d1bd5c952e60b233bb83628da2161321d09ab7794445c0", "keyed": "ac1ccae4705f7f9247e0c6e92fbbaadb4c07dfd817b81a0c1ae6e6c3b8cf10a7281ff442a2991121b383029ad9fb0896965a77a4f8c0ba025eae95ed1acf98fcd59901212fae11e458be939b28ad7ab45eb0eb74e2c54b8fec25ed0c8dd2e4c5f785d98b07604e03e87f4936459f078d05c9ad9c01e1be625e61a982b6f01ff3398d47972624ec97cda2e421f73ecbc9f6c95feda44fd303c98065c1b848b1c623fcf9b2ddf7a94198894a9733731b5f9b33f93a5a502b39cb95d7a2c98eaf9de101f8b8d1e01706", "derive": "18b00d90ca81bd1c666c78c35bd781310aac74c7d982d6b30569bdfdfe157d2f2f41163cea7067b9d20572c946ac04344f8f680ce8b57f5c2f16e6a5e6b9579315283b0e661dbd1c873aa1061fc92c9e6c173540fad0a591d6886ef2953c9a6b5d2aef3eab4955afcc18e287b3adb4127b55febc507fa290746c8d5ccea4bec4ad278b3c06feea13725fb6e527d7e5956b65bb5743744117ae79d9a604055be0425cbd0007b127279a54aa09f5de423a72ef5b3d1421f5ffaeefb00a9adbdc338478b9d80086a385"}, + {"seed": 46, "len": 127, "xof": 16, "key": "3a7db0a1fefa0fe7771a95a8b9d8f21e7f7874fefd179b050e46904740340539", "ctx": "lambda-vm oracle review ctx 127/16", "hash": "4778a894166f5af5a67a7efde5af3adf", "keyed": "2dc7c28af3cfcee3cf578e4695d9b138", "derive": "f7912e7c9bd409ab95c01b5b7a068585"}, + {"seed": 47, "len": 127, "xof": 32, "key": "ce178a1a3b9de3ce2d077eef0e0e99be95106dfc3c0c7c89d99186078a9fd209", "ctx": "lambda-vm oracle review ctx 127/32", "hash": "cc328f5912ecc945f8cc29a43e6091d914392a54af859f09607d836b79a3e745", "keyed": "36a924463ef891bd94690c46fa412e395bf7bf812a9463645dd6135f08b116e8", "derive": "2b0da95bcadadbc0fb612055ed5370b016f354b620b1a5b7d108554bf1986cf4"}, + {"seed": 48, "len": 127, "xof": 64, "key": "7b57a2d7709314e0891179c6df8ed261124481c5a5619e735e48bf762831ab09", "ctx": "lambda-vm oracle review ctx 127/64", "hash": "acaab09bce3654c8398ebc7d7253b4b103d22133b8e0c880612b59ebc0575a1a786286c8b9278936372b029a79c963e10046142c1dbce65ec9048260cbcf6c5f", "keyed": "a095b8ac8eb13e496f2afa818bdd18a43bdda3959c2f2dd45f45322b6d0891a7c6ef9203ad21ebfb9da8463f13dd3f92f7bc461bfffe8fd9c12e03859e5cac1a", "derive": "a4712351b39281e204f97908eccc9326059fd4498f833ee9531bd083c567b1506dcc1170eed9ac0a3a88fc6fd4d8531bb756d0b2925ac515a68ef8a46da24bae"}, + {"seed": 49, "len": 127, "xof": 131, "key": "a103179229a58b5516244c74202acf3458ec9c505c675fcec68f583eb6d4610c", "ctx": "lambda-vm oracle review ctx 127/131", "hash": "997bab908b56bdf7ce69a0f1c6a4eeeecdc2d5516fd7fc18cf29a959fdc0e85000b8074e90d17c3fe63213476aa17a193b6f42e1a76db2ee05d5b511ca44d59fa84242d12aa698a21b63a7b96c899cbaec11c95214c0d55dfe5465c9f977c02ace60856fdcbbb35a7c591da0a1cdf5a9cc044c0d87f778acdb258fb21ca6617b51c29f", "keyed": "239e5db4ca6fe92c80f9680341715cbeafd6733cbf111248088215b72223553f47aa63c55e7a15bb8a9802984f53fbf43033e3c23b334fa16a06b59c18551ccca47b34c4f6fbf1c820942efd2d55cac5c3f9df826ebac493d70cb7dcbef7d4d2277a595461d7a8046c544ae779fc327efc6f3e56bc16ce190efbde419cc5b03fbe4d8f", "derive": "b86ba40d7ae3eccbe5a6d9e4533e65326a04355d58d803e97935b524b937358c9ba594a6053e38d043bc2e15396a77e4c1285000d8861244bafb12f66e740b9477c80f5d980da2da1cb40ee06fa3e2f639c8e7e37ce6e217161efaea2196d1d3d8d22f11716efa004cfa19595d44f78c084352c0b99062584a8d9e7eb29292f7cd7ebb"}, + {"seed": 50, "len": 127, "xof": 200, "key": "a480d2a7c82d1bcda32057f4f418bb56e9a6384fea983e669b9b9e75f065e6b8", "ctx": "lambda-vm oracle review ctx 127/200", "hash": "9cb06e7f440251995a8b91c1f92e5ff1047860df725c557372f26d4b14f80c4a7b5f294659a344c67b04affa0bf0a0872f64c7dfbf2c8336c88b09dbfab82250c389e98dc0c0a92ba3c850fdd7d14f2e0c1d3b1f8043d82aae6216a528dedafa75bbd034bf977dec016ca5b943b7fdb063fe1a4daaf7441b66ccd1180b87f3382e262ae1ea5e864424a123ff1c4886f98c8c219a26d2857a5724dedd58d483d3a7b11a436f15ff94e7d179516e6b900e0fc1978f53048e27b7d28ca5ccb6a0221abfa605fc3b1616", "keyed": "d7c39ed0632dc56e37158d34a01fd4983b4e3a80788e96887c7e4d98e27962689b3ed261b26b198b64919703ce60a32a620a67887fd956a03c9b07cb3d0a0bd1dcd42252b3ec4589573e70c5f7b28c78c9ba340b7984865a276ab331da59d0a7a5400593c4f1f55a9b3e6a0b3e7c49459254976ad5eb514171662d6eb7700a8332bc233252c9bb11f747e741df39ab0d3aba1202446e2bfda08f8ee69b77fc4d21a5ebe00096381bc15bb9508fc775e826d1d8fbdae063bb9780993e25f65c60c2dfb28bcf9c6f41", "derive": "e1124d9e220190870e3f796d4658bd050485df3008274f1449cd0d63b2ad056810a1f5b884141401aed964d6430d3ffd9d204d7e7b5fb0bf9c523cfccb1be6fe0910b9eadc21ca3dc8be146a96de93906937efef45a1b18c40f5a69524ecf651952f117fe8df66ce769e276736208172699b23d4db026d199c587ebd3b5cacd186ba696e968ab96ae32f4f5dbe52d15ecfd5fc6eb643b86058c1e1efd5df0576d1078126efd431f4e24474a762a013fae60bf874a1e75c29b7055a405b0c6dce05651fc7107c4e6f"}, + {"seed": 51, "len": 128, "xof": 16, "key": "ca2c9331b09bd8318cb472ef0ff2385f367fcf277359132fe18225b14e18293d", "ctx": "lambda-vm oracle review ctx 128/16", "hash": "7260b268b8f6338baff4c78c44faf52c", "keyed": "e5f4f67b8657119ba6aea3e641564caa", "derive": "9a9c46b5b7dd0b724aa8b0457b7ebc56"}, + {"seed": 52, "len": 128, "xof": 32, "key": "16dfa94a87ffe443e3dec0dc13f3bd6d19796c33de8e376ba772f551b9e59d8d", "ctx": "lambda-vm oracle review ctx 128/32", "hash": "4a63e95e25c4670d94ec1a1e01459041794e05b2e5a9bb1a7818935564290013", "keyed": "5973445a97d555a406ea562b257248f44a478e11d3dfca0b59c83c8123e60557", "derive": "6a43acd3d3798a352d137db9e0afdec0589c6d00b5b11d4dad5baaa4473f89f3"}, + {"seed": 53, "len": 128, "xof": 64, "key": "aa0ab7c74612cfe86fd6b0258a8253a5b40fb2c37e3280d3c99c9d641ad8046d", "ctx": "lambda-vm oracle review ctx 128/64", "hash": "94745003839a79dc8c227e95dfa24e6ed4de58c15b03360c8c16b4c3476c7a191a9234fa8180c5dc599f2664a94eb7d38253cbd97114942301d3c24ad0375f74", "keyed": "c01eab4bb9511b877e0a9b1e20c47d99a9ad9517297eaf140f5503a5becb4773b121527af1060e320e51ce5c60330f3f59096b08cf7794d828c3e12605d53f0f", "derive": "961479805b01dfe530e97214ea5ea55ff58b2b1a0b653ed7087e2ed83ab50ee39548af3dd3f50de83b067d2b97546bc41d956244e9e58a4ede6266dfad32950a"}, + {"seed": 54, "len": 128, "xof": 131, "key": "3f08fb1a9b5ee03886bce106e723704cf0c3c1258e196572e9bf0fa2d66f70ea", "ctx": "lambda-vm oracle review ctx 128/131", "hash": "1c018763f8d48fc81bbd5ab76e8e20b04159f50e19fda1406aedfcb712f9e018f9ddb4d9bffbbb29b38ef100700c68c8fd97cc14b510b77c746090620e4e1198db41b55e25ff6a08bba111c0287c61b5be00cac4803954d604ea293d16b983f53a3287ed43ed1f02b64debf46941a8273a3762cb81d54bca602dbd02dd525cbbef0ed4", "keyed": "3b46b8a4e038f114a6e4a0d5721cd4ae79cbf2dc28227ac353fa7f24f6c0248fc38f39da6fb574fb82e138c47f24824860136fe20023e754c9e603e20f2601b1ed7c29d52a9e555c681b663927e9f168fc423d077188224d49167f7c15631a317af5af52652ca91133285810e646a7efedde51452bcdbd889a296c821c77cfad74d625", "derive": "41618dc6fe81da9ba6d4a79f10381a6ac4a39959b1741082b18e9314fccf1e82f6a8ca667aeffb4bb6430fdab35f3e3df1ba578fee5debf55da9a1d347304feffe5139864ba1bf6a427adbecedc3f80b197f0d67ed92c2a8e986d52dbc0fc01aa7dc92f7e666e07e0cbb7ddcb45a7d8843d1ee417d97e3a409bce4304f36650050f572"}, + {"seed": 55, "len": 128, "xof": 200, "key": "d3329a66c41dfbc16d0b8e80fb27d046b6514604d87510ee726c05808310379f", "ctx": "lambda-vm oracle review ctx 128/200", "hash": "58bda839d3594b2568a91d58139ab02d39a296ffb3903c766ea08efb4c94d3f360c990b52efa7d0b3e5ea44fdc0c31563aeca3142f4cfb711f24739b2dd20383474d2c7999c39954ea4bd37860ab1903a35b42e826551e619111bae4d0a9b682676fe2ef9c12d1f55706ae1b070a59485045ade6ec1e7c335b1b4b98b5d7b73fb3f17c659996cfdc79054b02be7361b088bb2a27044041021949e37a147bb285142a218a1c6bae7c7a71a586822f1665be830c46e055ce443b3ded25793b8d151a1e0b0c3c031ae0", "keyed": "d3f6bdd9f8574460d01f25f2cc463b007cf28812493d05531ed305963789338306b85cb83986e0923fe12c1a30f79326255ba8406ded0c47cd6116f18e5d09f299fd7579f69223f91a546b535481a292948c832354dd4a823c2b458b5158f1c42a6fcfacc8ff5e35d047dd0a45daab8ea491e8e9622ce71ee4314213c0487c14dd7ef310f0461f67556243c5505dc7f73a5ae676b2ef017502048f2fb9ca4f84240bdccd955b6d2c9b50eb12dd5cac000d08b33d1874b910bd6bc808e25b75168476bb04ea76938d", "derive": "6450a7dec0869a1e3c8acfe39112f70f3d0b3e8c296f0c3096235b2917624fc98827f8cedff426ff6e189bd360d64d77fee2d293dc5f62b8b1a0ea90b73bfb40ffc5c139796e969bb589ac8afbfe8b8e501142b8e2d966d70e86c87de016ecf7815430225cfde36a706090c3e71439836ea38a481c6bca101ccf496de13b2ae4919b7d9d426c9d41560c598b624e45b366020a850c4e135f1c44e99d978f4d99d02a076c51080e6e8041552a9b9ea8ef6df548613e8d1115a40643c658a444698a75cb7a334481d6"}, + {"seed": 56, "len": 129, "xof": 16, "key": "8ed29fb36819743a8aafa7633b901089aed972c78f4e659dfa303fe3e7d5f763", "ctx": "lambda-vm oracle review ctx 129/16", "hash": "53933218cd3a7dc226dfda7657611642", "keyed": "f9806797d6ff511f653fc782afc27c5f", "derive": "bff8e1942bdc60787105dee8ecb3c163"}, + {"seed": 57, "len": 129, "xof": 32, "key": "b37d838c35afc413d4f4a0834775240e29b5cee2b4a4fd4cf076fafb3f075341", "ctx": "lambda-vm oracle review ctx 129/32", "hash": "788921dd78ce7a8af5f477677d8242ab2c1fec000dbea6e20356105594ef6977", "keyed": "6719b59a5e51d0a42ae683629e6d8b67c5c49a11cd69a78ddca2fba1af7767fe", "derive": "720c8d0efea80152a623dd2b67c8e707fbb93f2dfbdc6b2d39ddb1d649b61ec5"}, + {"seed": 58, "len": 129, "xof": 64, "key": "d9fd0cc1961360d4031cd4d5f1ec05cccb77565ef4b82eb4ae4d6f9a7883a731", "ctx": "lambda-vm oracle review ctx 129/64", "hash": "661f362fe1a4536f81abbb67e8c8505e9ecb0722aaa843a51f32613db21e4c16f165d3d237eecb99260e3ff88f8b43f3cacd0e323a9d0938e4ccbafb3212c4ff", "keyed": "b2b0f1978f87ad14a9d583d4cc5512192e15a3e9f425213a44895cd9ab3541023609832d8bd47a53d23d6a6ee091a860c8e5d3e8171c320c7d8374aa950b5a4d", "derive": "5dfa9b9f0ac8703982bd9f1dd20910601acb82b9d5a4cdcce3971b4fad21fcba9b5fe2ea7247dbc4f0ffb21f46daa8256ca0f04eed428f05f544ccd34b1754b6"}, + {"seed": 59, "len": 129, "xof": 131, "key": "ffa85eed76fc44128bcad9bcd7ae3713c2dc1206f3c61e0ce2774834da5e0617", "ctx": "lambda-vm oracle review ctx 129/131", "hash": "5a7a52e7bf5e79814bd861e2cb80e3af706036b544f8ed5e84a57b32e0acb6b641d009f9fa8d095ffdb0f00374c743c1c34edf3709f88cd33ced94e99887e3b1c709f0b835eb79c8726a36e1f032286d70b474843b87f6be8fb55584ef12b25adf776ecd544eef2794e72b77821ce05ba955d39be3ce18e791506e904051747883e6bd", "keyed": "98c57f6aa22ef67c09b40eb6c9f25133e3f439077d6f44669c5fc1562276affeb135ac5a030b438549803e05f07c384b1088cedd3849ae5dabcf2fce16fb7bac3065ba7b03de9b3c50c03508a29402463aa0876cff9604a71b8537856ddd8b49b0375e3e5582cd149293680909f40dafa59e7efa9ef0245ab3e810c1702e5edae2c261", "derive": "261ded465c98c65aa2623b43d3661f1dce5c48276fe0288ddfebd50967877716f60ec020080e6364f3c9577f2c0fc806b960eeb0ee61d96aa6e3340246e9a089f9bcc13d689454eb14a6994f5e6aeb2b5efd1d7b478557faf83a6b6da686b8a3f0aaae6639a66d948c3f76ca955b9fda44f09500e15cc75d98b999f2f72db52a63278f"}, + {"seed": 60, "len": 129, "xof": 200, "key": "285a6026fc81bdd9abc31cbbdea815f5ea45e1ec71d6617912542068e54634c1", "ctx": "lambda-vm oracle review ctx 129/200", "hash": "1121fbad05a0ed2ba8ff2498d45a9ea7e860384b021aeabdfed3c67f0eef7408ec6a15ec53723904e10c890e7323d1b509b3fdc0ba8fbaba2765eec1ab6db11f0feb360d1c92ee019b83a2f86aeec5554850ab924965ddd94d294cabe56b1db35efdada8d81e0b8832da33a47ec9ce135d96179ce3eeecfd71562187ceb9f5a550dd006d17f71fbd83826e1b1abbabb9719bd4a43ebfbf88572a9576d4e91fbb9da188de5a7cf2f2e2f3aae7ccb3eab767c49f82e127b172fafd76672179638768fb5fb06e946647", "keyed": "398d09d5b5cf7402631bc8eba3a4d4050e07a814376eec7edd52db6cee980fc104dcb8b8efc8a0132c2749f2565c78bc19db1163da064ea20a78c344c2219203fef359e4e5f9a40bbec9d401c14f041be0a034046c424d1c7754c25aa41f7ec749ec7f47e461a6c676b19fe4068f925a7d9792539d4e5acf92d16015ec05e2f4d889eddf55dfa5155098ccb903dc871e54fdbc09e83d546558db1ec6902c0cfda170f2fc2de829195766f65880fb8658d993301976f0e9f1799fbb5d27d38b344ca4d70b91b54b79", "derive": "1172b99ed6904a716737def55363442c70ee1ca85a08b6e62bcd0b1f15418b209babf8516c130a4732480acc4ca3d052c3d1644b71b7c6785d70083b6bbeb1945f4ff6179f9d80e177f44fd956e6ee4895807dfb70e7c8bbb8771bf3447f426c3e418af5bf5142d70090b1de34d2f1367aaf7f28ac844227bcf7633dc9c7f1c220164e9b70b00f1d26abb97c3e4f0746123788739224119e6071d4d7fa49892689c8e2463fb22ed28efc9207f7c64f2e768e172362e3382fa9ebeccab5f3d1a8abd7cb3c837c0a10"}, + {"seed": 61, "len": 512, "xof": 16, "key": "bc84ddc1f4c405a0d70a7a20b0a65b90abd9c1523a575929a31b51356a1406b1", "ctx": "lambda-vm oracle review ctx 512/16", "hash": "128659308e8a7103b89954328ff3dcc5", "keyed": "66ac5531db455adfae33d01f2424b2e3", "derive": "3714887158aff8b39e20460d1b1df7ce"}, + {"seed": 62, "len": 512, "xof": 32, "key": "74856534ef4035d35cf4cbb65aa0513428d5722c34950e422b79253741136300", "ctx": "lambda-vm oracle review ctx 512/32", "hash": "61e3770e80dffcb46dbd31d0f0cfe311cc746d2d051ce2fad4ce235b8249b6aa", "keyed": "9701c520da175568b55b68ebb9b9e38671c9354befc3751fdb9279cc6dde019d", "derive": "f81137c37343d28421733991ceba4f7a7a8445c58daf78a5f941c48935091771"}, + {"seed": 63, "len": 512, "xof": 64, "key": "08af96226b264d763b57413e3bf21605001c95eae5ea05b5c43da130647b664a", "ctx": "lambda-vm oracle review ctx 512/64", "hash": "ab098f1a5b5fa7e09c5681c5091a5c160ff00715c2824a4a2633401521ca46ab86239dbc65f0397dfda02711e457be739b40a4fd03bcf729503e57885a7bb1a1", "keyed": "b48711a18f51ffe38c41060057a47e0174cec29eaf16b10a0a6613cb23fb5b06e347e3ddbba4a2b6dde67587561452b2dd584c133bc5581062fe9f97b0ed98f3", "derive": "80ded338ae4f1d27505253e93dab1949bc51e8a2011ebfb24d99678057b6a18a8b0c7c12eebbc78bc7b343566b8fc7eb4be7705f7e975b35af72dffa911d343a"}, + {"seed": 64, "len": 512, "xof": 131, "key": "96f47b23ef2dd2dc947462660df783ddfbd7c45639e8d2107bdc5646657c5cf4", "ctx": "lambda-vm oracle review ctx 512/131", "hash": "c735b7a9e0afe293904be51a137cff72222202ca676435ae5b41efadcbc12653b85688a678e80d95f9109c10c689718a825311d8e99f5cc1f30bae70420a8e2ad9bb1b8fed710c789577500aed255249ba6ed21abd355de74dfee33d3648bcd439a79514b64416073c7db4b635b107a002ab71c9019de007996d51539ae9881759106f", "keyed": "f9d0d51d41f7a7916272ae75ab5330a4feeb92e927bb53e948ddf5a81d3cba6ebb757ccdb34719226cd4e425fad2f762cedacfdb57f65a63e9169cc5c35c46f02f37820e2d3087b3fd8a64bcd28d67df0ec9ce78a4d2caa02c0c80d43fb005a8b73e7f265c108c72a47baea9532698639dd9e12cc6275dd1b03c7bf687b3bbfd776f1f", "derive": "f5c3a9f44e179eaaeb3380f48f03fcc9a84ab03aa7db263b1580da1c6e3ebaf91daacf3cd305414ea5d90f2d368133acc4b579e6b84cade514e93df6ad20f9f39bfa10a796eda1cb142da2790e445294e3b12f3360d1f65bca819647c07234505d1fcbc63f56649047e4885bf1713f62c9a6b66f1e699d6f2324d0161507997e22c42e"}, + {"seed": 65, "len": 512, "xof": 200, "key": "bb310801144662e300b5f7e356d7d4fede0efe9a98aafcf4ff570ec3968023d8", "ctx": "lambda-vm oracle review ctx 512/200", "hash": "6fb76fa8d36be7d4f31b0bc7c952282740b91f430d12fdf645d2b5bd598da9c2fe8f616b84594763d2421979a7c0c31932952cbf669a5ca8e60ee3f3457c9e21041cedf59045c06e31625ce457205eabf5f7f6a9cddb34a5f79a7092c084915d509b7bc141d29630c9a520277a0bd5044cc3a2035b45464c928bc5e6361c9d2aa1be71be9f88406a2cb8a015df4ae3b0e44fbc709ed80d019598936df42e956251813351d4f7c420914363cab352f7e08e1747cbdfa7dee4c118daf1d6cc7269c34ee7ad2eed2b29", "keyed": "35a85673fd6202f1e550f2ee748e8d76d146134a5a2e7e503603d934fd884cddfb0b1b96046877118ce52cec5459f414606fb538cb75b3373e975566667a7088640cbd4b746faa3705236012ecc94b9a0de54783d679e87587ec9e7d9eeafc515ce532a88a68746fd615189d81f638d3ab2fa1d82a09c058d7a2fff2e91fffbc4a307f2836203f9897b52696448e041bc5628fd6063c43d480c4da2a8dcf16b2ef0cdbd039ebe96a08ed64f3470a026399a13d70990d6b569035521ef789471b904346448defaced", "derive": "06ac2355277dceb26a410851af772954e2e94f4ac8f2f43e55b06b0baa0ae50dfb33744efe8e6ea2aae1fe758c187707bc4975d381e67e173714267ae944d6504f8858e5c9f59a56db538ff002c7d0f305ad253951ace5e363ffc5652410e8ef74aa31cb92bdc3c370ab562dbd0e2094f1d687602ebcc0d3f9a911e40f1f917bb9937dc0d64c6f202a0f051ec1dc9bc9c96cae56fd5618ec9607ec7753a42b7e4e9f03ec73b4ac3264210c2873fd5caf468eb00dd8520e93f7b049dd2f05d99458d7a53126193407"}, + {"seed": 66, "len": 1000, "xof": 16, "key": "befa8b059d09f3a8dbc368577b4d33676a30b41578c029b9fdcf417ffe9a43a2", "ctx": "lambda-vm oracle review ctx 1000/16", "hash": "6f7175181e05840cf5bca9a1d59ddd39", "keyed": "31b3ca8354e46ac283cdfa7de0becca0", "derive": "3e44e34bf6578e1ff975f05cf77e3510"}, + {"seed": 67, "len": 1000, "xof": 32, "key": "e437cdb211ffa77321a331a3bd48759e26cd010375f1d68219457be4fd38b6f0", "ctx": "lambda-vm oracle review ctx 1000/32", "hash": "a7a6ab659e13f7f98a6c3500998ffc3a17416de7c9d5f3b3163cd123838e028e", "keyed": "ecb1d9e27fba7b612c9fb8a4f4715706b3b838a33c357279128fc49b9c454a40", "derive": "3b541d49dbaf94f167bca1d31223ba17009363e27fe1527b771392e0ec2e5f25"}, + {"seed": 68, "len": 1000, "xof": 64, "key": "75c4430c6cdcda06c095d3f03bdbda5b779e718476be00d654c73ac6330d70ea", "ctx": "lambda-vm oracle review ctx 1000/64", "hash": "89fa21877d363271797bdf327d4ba0f311139f593f4ffcaba00d13eabb9837203ae6f4ee806582664a8160a4a065346c6f923a6102e0f9606889040b16b6c36a", "keyed": "5b9f65641c5dc2438c736ea0207ea79f801385078ad64dd33afd2dcc8fbd101e90896b4c672eabefb1be15c3a2908e6d68a6b0d2789614aa75a1e771d3910800", "derive": "ef3972e6aa2c92126b3f8a982bfefb489d86031239d7d6da94c742d533016c602e5b053d60df3549775864d39350ec69c80b38178e0c297184cd014248c45fed"}, + {"seed": 69, "len": 1000, "xof": 131, "key": "0a5daf6694db1d9b7660c1b126fc81066d359c5b5142a13581a2f9a542d40a70", "ctx": "lambda-vm oracle review ctx 1000/131", "hash": "4629528981a3c2c0b64c229c8df8d050ff272caaf4a415032c625bb8e21538174b8d38c7e549836c968ed937cc98298f6deeec12ba4811da87545a4c831a965417181f7b5c59b5b60d1a2ab5c6c4e736949fa5e24c193a61025eb0bfa574fc360e0b237107b3922ea180fb81c5e694dbcbf10c5f541bcb085edcaa56119a3fc5cf3e5a", "keyed": "686683bd2c69e03888ff6a9249f7fd27a793f4573f129bb1aa2afce4d38d2e3118ef6ed035d090d68bc0cace642f168bbf4b1e14d382817cfeef8c51c87ae63c356094a40cbc2d3dd48a321ae5d988ea4c0594390e9277ab97358f76ab01790a841449d04340ec9e2bed929def2490f6d32fc831db678f01466f9692b7b435ce84255c", "derive": "7cedaf8af519c0efd6c7f7b7917b512cf33e3ee555262625041131f660ae4cbdfe96a4a81a3f14542197696cecd174efa1754894b8c30aac8b382b082f8252ea0060d11945ed3e3d6c94912dc4624ae3512551715b4e18d2d74ba41df6c633839f8b33c846561aa1ff08db9631f4ce0bfec8f7ebe94fe02282b72b31e32c373b4b3d59"}, + {"seed": 70, "len": 1000, "xof": 200, "key": "9eca31ee091f8bc6ff2a265069d32a2080cc6aadfae7b2f4473258faf88ea762", "ctx": "lambda-vm oracle review ctx 1000/200", "hash": "4c4c68cb630b73370f497003ed1fb20d23540096ef693cf499afb67ae5285bca71a17877c8c5fce6ed6bc9a78c42cbe77a978f3684ffb6179e619d3d9a1df86145546ade247622455b41af82eba6cd14fe268ed0d8a3e7d50675d796ac8464c42ad20edbee74e584441150b3b77ecb5cff422d1e684823f5b947aafc47c61400696302e36bbc05b367a09d229c153761fbae1604ccbc1e7d9cae9e7f0e938879405c2525459c5b98431c1eb70f8fffe3e909d5b5ea8027eac22fe790202255f1925d3274d7ddec2a", "keyed": "1abe83b666af57dc91ac342cbb0f1ba56490a53609cb4c38c3297f32cc31a919c9ed827bcbaadb32d861539f7920017aafe4e2613fa2c042bbe734da49d296e75bb5d0037f7daf91afc9ed8ec1eee2acf65fe9f12e6d2a43d0b809a6a67d77a28823189e03fd16ea2acfaece5c5b6439322d5d5456248826faef2feb18066953aacadd5391a37c9153fdd776f5dcfec1fdc831869a711a61ad54f677c10467df28362a1ea956a7b8c47fe50779e17700ed3065a9aa0a7b1b8e07c47156afc8d101b67316f199a427", "derive": "31453aadda0b9fc43f8f9e3163c11135dfd69c894e5924e9ae45c17e00466abba8c86a85f3b22a7f6ecdff2c2b8f599dc5dc8ab35b7436e70d922425b598ce2f17f6a10efbc0cbb7a1673b70d8417583a264647e1ccbdc8e219408986b448c7a14aed4e5191587a533eeb4e6fd8b493556ff5e1c28bf8994868674abeec5e1ac8c6eca2e04ada8f24d46db692784bb5e2dd336d5b24e28dca09d80a5c7888a2123cde8183f8ccaeae5225426e8932a1c726ca63e962d2236581c12abda25c6e214b45ea14bf32ea9"}, + {"seed": 71, "len": 1023, "xof": 16, "key": "33630b173362a69dbae2c07065866154f76f244e99f1315197f109eae4b74b56", "ctx": "lambda-vm oracle review ctx 1023/16", "hash": "21d7ad694e7edf37046214b4f872c6d3", "keyed": "9b73988949059085d68ecb8518945058", "derive": "816ad3aaaf4c9ff75502cc055e88f84a"}, + {"seed": 72, "len": 1023, "xof": 32, "key": "32552bfbc19d7b92b714b972065e0a182c3846351fdbf9f8da473a66d07bb089", "ctx": "lambda-vm oracle review ctx 1023/32", "hash": "7d4502d0e2d35b69bb2b6cd2f5ef61e38944736c200ed043d7b2ad52310e7be6", "keyed": "e63ba15cf67e4788f9aa504e42dbe78dfdf51de51aa5fd61a20bfea0c29cabe1", "derive": "6c90bdc9d0743dd9a18bcad1dff79d71c117b8c238fd54ca0af0b74648eefe0a"}, + {"seed": 73, "len": 1023, "xof": 64, "key": "58924bf7db31de95950c6df116b7c3d92d800d4d9194e144d568a96cda1f5404", "ctx": "lambda-vm oracle review ctx 1023/64", "hash": "4efb0d84bcec540b6313a0c6da16c19f6c548d825322430a54b3c1b15d60f17f3006c0199263f3183180a80c47752b2b890269524d0710254576dfa13fbd75f6", "keyed": "58ab643960e5e2fb588ae0192a44ee111dd1ad4ac3edbf46c4b56f69da3732cc08d70a89440a7cc3301f7aadb307a77378b569aadc756f45269eaa7250bd4e01", "derive": "b10359e0c1a9645b79fc3b3c49a4c0f5b8b4a33739e38572521b1908cc3c8b857349e672cc8d7f976b91ccb7bf37ed104cf6de35aa81700cc69123d311651dbd"}, + {"seed": 74, "len": 1023, "xof": 131, "key": "7ea3111b6bd050ee3b0f5804f3b26260e9be092dba7a3cedf86e31c2064c676c", "ctx": "lambda-vm oracle review ctx 1023/131", "hash": "392e9eb9433f4498cd9d9968ab58ba0f7a3a0e07e2187f0c5e35b655ed6e94a1318e6cbcc7fcd27f09a4d48eea14feb22add27d001c4ba2e331b5a8d37264fc034ad4fa8431284ebd1aae6f9b7cdf5e12862a46378b3bb2c94e8271e71979cdd4fe21eb3b582e83db6b50f712012cbcf904e021527d1835e06c99ccaef73a356fd72b1", "keyed": "94fdbd4d70e4266087dc015d7c404db05941db771b69b5958a8d563b2a59736c8df29b4f1462f39a868859446287e2a9f9193f616097f2799512d501078a6f2f16dc396e9a4aa7e42bf84b78d6a83fcbcecefad6cc7aa7e3d08638f4b69f4dc09ac3ff4087b95c90db8aee339dff663710f152c06626244c5ee58c75efcafd9b739f84", "derive": "30a5ba2a68b85a8a76ae8eb44b657782f4b845173d98169b9d79c3447b8e349489cc3141ea763b61d7e0cc825af7cfc9d3d3a78d86437b2e41531902bee1ddef2f5433032e5461840f6e89c691172eb486da91c1536ca59b4403baae9ec6eff03270b5cbc3b11d068b98a4614e3fe5e0354d10983df886cc00c4b98f1fb4eb2c7b9b9a"}, + {"seed": 75, "len": 1023, "xof": 200, "key": "a4dfc16ade4a493f07e112ebbd4a73ab4edfd7d7899761ccebfb8492a295b756", "ctx": "lambda-vm oracle review ctx 1023/200", "hash": "8478f370e33d3592d7991f12964bc4af23d387bb53aac03930f9ae194f4379196a814ec634008448bcde817ab14237fd0e6869649c3fdb74ec94ae5b1c0a434d0d5b3c60d38e53d07da811aebd7195f6c8bafa2a52460eb52ce7d64201c59dff2e764bc643f3579392f90ff9895fc5c573df0633f638b05936fd17fa84cde873e5b95a57e156073c2bc1ca48048f7078bb4520b81d7b1d4acf9558817e024c9a09872b3bf2260909b01ce4a21c594c91d579457749e0b0b801fd6c9d6b9e466a59d621f78d407ed8", "keyed": "17c56f3fe4fdc6d0e3df2cd517df23c1db7e100ec5aa8745b11007608c32c9a58a83573685159f9724b28e13330841964e626f9fcefd62a12314fb2a3f12ddc30d246beb5093926d199762853846e8e2cb050c44f6ca11cb4db8e6a69e0ee7e9c44b8a78c9ba43b6dde1a31ecaaad50d732eb22a7773ecae84098a5f8915562b5bf9ec1713e8e8a9d8d6daffe5e7b2daf3745a76ba1f2ef4f1d459f2e91103b89977e98e5192d10669d6351ed5779a9b44506fd5a707944179c2bb631c1dece54610f6981fe082ee", "derive": "bb14d0c8f0faefb4a6ce5cfacb240ea982419c862da6463a0feb08fec167edb8aabedd5abe2584b5b44508146e47399173927d08d0e05f2a442065c7d68d135d2bb4fa39c3eed3e5289074a695d4b7fadd8eb55f44aeb50195ff2424da1a9b0d34e7dcf73078d7a90414e1dbd603826596763990e46385fea060131d2597736ce4dbec61a6870d9e3b7332c5d9f5874fbede6b078646c6d0fb003a1f0435ad76aacaa47b804032622589fb474b8473e67794c42d99d5360c39ede03da8dcd427307bde1d74bfd73a"}, + {"seed": 76, "len": 1024, "xof": 16, "key": "122524e4b147ba890bcb4b36a1b39dc0588f4c21eb663bd20397e4b0cb2cde27", "ctx": "lambda-vm oracle review ctx 1024/16", "hash": "22c2bc21637d5a3a5c5772a2b41daf34", "keyed": "7914fffe0f698d80bc7ddab03e05c6fa", "derive": "076f9fc6b3a35312c5c79c687e9f1921"}, + {"seed": 77, "len": 1024, "xof": 32, "key": "a7be215c976dfd1555cb25917f28d872d4660e506ddac26586e1d1547ba4a5e0", "ctx": "lambda-vm oracle review ctx 1024/32", "hash": "9d05e80a80d43143baf493decaa5f031fb91f3ce6fd81bcc0270e9473ac94d6e", "keyed": "c3b7affcc71c6efe4fccb3278d0d41acf8085c81170a03470924311c8c6f6167", "derive": "52a053439dde9e4d68eac7ecf2e32e8a3baa17f6ec0e291fd0338718806a9c44"}, + {"seed": 78, "len": 1024, "xof": 64, "key": "5e72720477e1a7523601591e2adf103c8e5bc3c630a43315f36fb8a7cdf62e0f", "ctx": "lambda-vm oracle review ctx 1024/64", "hash": "d0abc07ee8290bc21e5fc11dcd7a2cbdd7ece80a5b916089bd023767d49e0a07e5ee1cc38e851dcf448494397f2374d5a0c370b6a70a2393a6ac06fd7ea6e145", "keyed": "f446907661aef0a2c702dc83f991902ae807f8c009fb16be9b93735456e4d41b0bb01a824d683c45854d11e2bce40bfe5cc82122fd9c465b06110fd966041fd7", "derive": "769c7cb81c0a7a115c8688f45902cb3573aec2266b8784f71443e657e777744a31a031cce9bf5d0252c5380b66dd9519b2c151632aa1a36c1e6790a09d97b094"}, + {"seed": 79, "len": 1024, "xof": 131, "key": "f20bbbcf5654a52a0d073e10a2c95a039f4c386ed0d8aae138eba6eb08cc443c", "ctx": "lambda-vm oracle review ctx 1024/131", "hash": "4e82ae97ec70084c61ae28f8f724ba3ceef17cfb6e2e89c4dd1f075a4d74f6743139e37de17ce3bdc7dfe370d212f46bb580cd659d445ce1a278781a2f83d4a06dac14aed49afa922506fdd0a59267cfc72ead8e7a7c0396428d9e941b6490908773bca2821de661219b6b51ebe33166d3eb293b95db50822e0b6f231f956bfbadd336", "keyed": "6cb89e82c21309efb425ed74278e6b7d8812d1f210b6ba85e7eecf2251a644d9670b8e05e721d60c7c9050ef68e722bc01cb59b1a3f91fe5395655e0805c1421ff3830033c234393af2a9abdae8b06827cfa9e01855e41f53fabe8cc47129cfa5a937530a337e207f86ab70523694c5071ffaf1aae614160b7feb18c271ac2d4c136a5", "derive": "0a9c479d7c63493957c01053e26c54b45ab2a302af25ce1ed7e820e82e4b787f897530604a257932d097a28518722fed68f570f6134c239c55f827141bef08930f41ef0c0e82b9acef19d4de5c661f101a09b8e8d80e47cdc189044b2151dea929234399774e12d44cf0c07390625cfd2cc17aa15eb24db2afad017904f3be4bf803de"}, + {"seed": 80, "len": 1024, "xof": 200, "key": "ff32ba83c1a39f53fb158d0e7b3adde55e6312af13b7143f67a106771f0ef5ec", "ctx": "lambda-vm oracle review ctx 1024/200", "hash": "4c7831baed8e7b961c5196ce58fe3b8954e90e77db26ca67cf7bdb66c4122a7efd33827be7372dbfe6c199374601eeb83c65ae54b3343b54d8fffc0b8e1a5af166e46ae44f5a140dd0d210f2e646099e91c4b4107ee5182028d32d66a534fa90dd9d45d4b54ee8fb482a9acdb0cf405711140e6944b619d4f185de6be35cc8e69157b876e7c6632c6dacce7ef3f591f1b45706a3d75744d6e6fcd8e193a52eaa273cba7afe60a52bb9c90772a3fb8e4004c51446cf31de17b7cac209e6033be62aef01cd86fe9e53", "keyed": "06fad5dca2d0c1ab104879bfcd86f10b9c2cd16a77cae797c40dd5584fdd65dd7003db899983836f9cbb8144c95d9dd5105fddeb5bd46559b6f89cd597e8a762de0a6814705813434c532380c9fdd6dd086c951a364e32a699f9dea809cade917f528f8ca390763079220b505be258c353f574ed30468a666055d60a7f8338137e57ca4d83b3e1dcbc86e30bb6a0ba69a9e33cee53bab754fc8c0ffcb4bd3592d9e1d6a274bb2d2da0e4bd8c32d34a3842fd5d3ce564d22ddb04bc35ff619f54250edce6c36201fe", "derive": "ceae53602dc8bda3ba5f70e37eac6c52e82380ed108241729c6def03ecb1c958b1dcfbc953368f85f1b95b96573bdc960bfcb1d23f86c7d4d05e05a79a9075707a833227e308173e06714ee3f6a24b2e620ff5daba96b8c63a3777ad6e50d3a6886465c194eb70406b261defad4dcc429813c4c46bae92b2d534561be169c5da0aa4e9b19ab67677cfa3262eab29ca8632484edb8f61bb6fcda32da33eb6bca12d5c5c595866a1e65695ad4f40a3a981e788b442fcafaa9f291e8ad4e7aa38119cff8cfddff5611c"}, + {"seed": 81, "len": 1025, "xof": 16, "key": "25de9eec69b24b07f16d278ec58ded50a93efa7241313709b81091cf0d732b97", "ctx": "lambda-vm oracle review ctx 1025/16", "hash": "eb7596ed1137e1cf9a7c4552bd016a96", "keyed": "36724602bbe0368b2123e429ed365a32", "derive": "b4f1243fc61d26dd1125b33a1c1b668f"}, + {"seed": 82, "len": 1025, "xof": 32, "key": "2838ed52fb997fd9e34ce40ab8e87831dead7c724b4254c8b59b6279a38b2c01", "ctx": "lambda-vm oracle review ctx 1025/32", "hash": "515d83974324f6b2fc3576abb7cb35d5a806079ac79b008183ab128bc687c3cf", "keyed": "17d066fd3a500a6a67ff4219bfb8607ea216f1c9d28373f287ed2c15632db630", "derive": "3485757d5c86ebf235ddbfa8d321602cb09e18dc428ba4170193c70b0f9c56cb"}, + {"seed": 83, "len": 1025, "xof": 64, "key": "4ee4408b9c7f1660100715784b4ce89905fc8f75be8ee48080dd392bfaf97a38", "ctx": "lambda-vm oracle review ctx 1025/64", "hash": "9ab339188c1e0f5503f7f464d22a46de9f6557bf26f6899f16cf6917ecd90c06823978130837cdd82a919b60a800eaaf284a6c910b8f1db578e5c2eb8885eb9f", "keyed": "c7cf6bddc2e55816047dbc1203a4d28ba617a5486b5a6771730edf6ce0d83863fbf07da183fead72816e6f986bd788136b341f97a1067139241acb9f11e736bc", "derive": "7e44d46c1f99a546c9352afd1bc02be2876b27664d1140c424e4cc244b16740dc62524e5a60330ab93d0faeb4fa3f2f6ae1a5e73b375b58fab7eaa9c723f4052"}, + {"seed": 84, "len": 1025, "xof": 131, "key": "9a751b6cc27fef7a0468d503107e27d8abd2f9c7779a39da42c6b0681f0c397d", "ctx": "lambda-vm oracle review ctx 1025/131", "hash": "67377651f2b0f5b916afb417f7fff4e3dece82aede4f89160be3a8956e41343e878a86f2352a5e7502583e1ad82173843897304d421f3e63849148580465a8533525c014f6421291cb028a1a84ac06e828e1d9ce378aaee0284e38e634f2cd9d74320302b4c1e0f165cc6cf90de17dfc9103afe60c8bb9ab81a062093a5d9df14ddd31", "keyed": "96c29d9fe588587498c961de7a23df82d564a8723ac5bc3e64d0bfe96b3fbb5a36bc482b4d9589b1421829b28d846e8df53729010627e3f855eda5e8fe3ca91dd83cc2b32d99e49fd511af21a0edf5eb56db0bed06584bf2c84a78ebfefcf12440af201ea7aef0c7f84068dabb3260ad3fa43eefd4b7015caa4bc55dbaec08916e9fb2", "derive": "ddecdfaf6e4b58a7e29d4508738406a4e5358e2492db8d5523d7ac7b4ec5b93f55d663fabe40000599cf1442159c864420d0ede939ec960b3c9182defd0d204542e9498a7bb769b828b181bcd75e36843db0480da80ee6b0fb698887c6c74c9991ea379564ca97b6e23ba329073fabd11dce75de6fc779f21670abaec2a74b7a8ba8e4"}, + {"seed": 85, "len": 1025, "xof": 200, "key": "2e9f9851066dc5a89e022a1b1fcb9dd3ea3ad37231117f01a9a30c393ea4d93d", "ctx": "lambda-vm oracle review ctx 1025/200", "hash": "561b9a7107a3a0ec41da537e6436d614de0928fd8c9f14e5e4db96469612c0461b5659031dc5b33f8f46b05c260b455cb4b400f1c16af79d18abb9c7ac134fca2d36a9024b31fe9ef4d3050628512ea0f945466b9c7d1ecb2a807d9f7e9c2611882747ed315e6e5236a11fd31ac672d36740ee41ac58db2330bc9f2efaffe5ee5ddf2253436013fe604435128ec434422b166e7b64a87c14eac7105e52e2c09c3fac5fdea6b3416ac44e29252c4dfcf4cb873488493a1aec0b9fa6ebe222744414c5982a6878d4da", "keyed": "8d7ad356afca0147cad9a7586babfb2e699d00d42c1dcf1342510b995beb3330ecb5d7f65a1170f325b0ab8a598fd3cc1d93e300a7124f032a5431255e931c56a0a90f67904d2996f01497d9eb3655dfeb7b55c44e3dcc3c41165d8c8608a51333a9474b6aa6c0c3622b5ee2261757381d12edd67939162ea1502ec17b94d9dea4ad33b42d8381591e1f91bd28f4f0b97f339c8d9e8ec07263c0e3bd12eba080fc4bde37413ecf9b7ba98ce52eb130185e219b826d8c747d132f58df61348f616fab68d72786be98", "derive": "0cc6758101c11c1e83655fe193ba15974da360aa10103233f08e46dd8e6dc9775ab0547d2ceaf644fff8b0601b04030793416915e97a0f0e5f0e4abf9a1d21e3d5b2ca252ec85be73fc26b22aa6f226eccddc2d7371498fa4ebe2e8f25f927fe57d6d4bcb7ae772146256a59d05e77a9e6e4710dd5f42605b0ca29122b9832b07e789c79806ec493a47313a088f17e30085f4aee3d90b7455a95f2024ebd00e096766724239a45c4a506ec3bc4ad44d1ea9a0e77921026537aa737350ae1a316c4df190b03e72b9a"}, + {"seed": 86, "len": 2048, "xof": 16, "key": "c37be63b7d15d6a397ff96ed27f207d7e29d9a22280c4c6e3147346f8b4c09a9", "ctx": "lambda-vm oracle review ctx 2048/16", "hash": "eccfe2bee6eca100dff3592bae156923", "keyed": "addf3e4da8e55eb85d9f0e38721f1f60", "derive": "be4ab18eeceb4e26cad91450546f2da5"}, + {"seed": 87, "len": 2048, "xof": 32, "key": "57a617ef49735dbededd8faa977a78443dcfa50bf196c96963bfb744972763fb", "ctx": "lambda-vm oracle review ctx 2048/32", "hash": "4749afc7faa42ccaf1222708f798c18a1e11146039f23da9f3b009486822f209", "keyed": "723b806ec4a871554612db8ce1c077f082793feb24af79da0da1a86d43d443db", "derive": "48e7b73f77f56d57dbda815fe5be0409bb668ddc0ef2c9774516ab27eca6b96b"}, + {"seed": 88, "len": 2048, "xof": 64, "key": "9c93f6150956b1b932501635387e36d88f1e9134fa430aa814181522c36d814b", "ctx": "lambda-vm oracle review ctx 2048/64", "hash": "e9e20cb49eab02a1b08dbccf5b09fd3b22cc43d84981865680eefefbae22584b3f53c0b3c9808b6d555e355e3b2fd4626dd5d00c4b65a781c1c7b5499969ac00", "keyed": "11ab1e09e164786a65ae13aecef2165c98fa89ef0567189b0dd8ab1ad6e78c4e2e5196272cdc58f02340a5f25dcad3bb2482c8e6b6e9b9c09fc181067046fbfd", "derive": "0ce71bf1f1bb53dc0c19f775da02b69d0bdd2d355f3c0d1a2fafb45551cafbefa6a15b3a1376a8435565449861c0341d37bc2c7cfc88958fb52b2555d7b707cc"}, + {"seed": 89, "len": 2048, "xof": 131, "key": "c23e6b9cb89f41a0c7d7e7bf22165dec740bac9f63895152802e43d3ac0553ed", "ctx": "lambda-vm oracle review ctx 2048/131", "hash": "dd91ebca42a55f4899cd0e25a0ec102c087d25c1b5c230ac1b5e8a074448720dfdb41736dffff0f5717f6c8e9a0dd6eda746cdc7f7634d4a69bbc82fad49f76ca38f7d58d8aa46728fd78231e9efc5d5249d0a412b2cf9b275c564d24c2fde7d5d968bfd3ea35d981e32d8f17ac66c4f0f29bc4dcd8082f324f1b1d6883ea2bfd5f6b6", "keyed": "457a806e76d666cd198569275734101273ea0c936e6b9380cf85ead6eead97a5f31a936c0bc6951aae3ca46178efe99f4d6ed98b83efb28ec5ec9d660e6b1048d8550b3001e29dfad2772f381b891577aa92e7f7d597411f537b3712bd5db8f7d1c5005699982fdba5bacd5ad66762f7d2b7fe2ec9a6dbc15a715fbf6e0f026100cdf0", "derive": "7819e47d406aeb41c0316d7db3f9d539ae763e768701334e313e938986369bb4297a90be5fbca72cb126074431248f0d570291c5906f71a01a479e66244d9cbfeddf966b83b53f578db9b5deb2568e62616142bef9edc4157c4e9e7fc91d3ad87f6c7340ab7dc493e3deaf6c4beee6278e100de4f90cb16e20c6ee62d5a7541ef9a5cd"}, + {"seed": 90, "len": 2048, "xof": 200, "key": "e8e0fe22f74b3bc9d2796d01e9a5461359ba22761ec3ae62fd250247808873e8", "ctx": "lambda-vm oracle review ctx 2048/200", "hash": "d1acdc026a5436d968dad88e6f04542b8d0df5846ee2a6ac387c44e9295e16df61c66897b623dd45a8033372bc77320274be1b5c38511ee24886b7d53e088f4e15ed3e140f5f99a829c4bfb70b70bdca42565e98bd38e785319eb5a4ee0bf5931a4bd6b8ee0b6e59eec5d9ea8bd792e426a1a56ea56e5545a8e8b65cad9ccf1d9678fd64c38f33879e17d1312359b2a4d314e8ef6ab2f1587e2ceb161bc4552df062416a58474ad3056defce3f2f6e7e7a175d6ecb012bceb91385930f8a47fa557640ce4276bb61", "keyed": "da61c6d28ac5f5f719ab444f8013ad9c56888df92dad2e939fc092438a0e83b17ac0824bea6c015330c7c3f2589a33e58bbb2ac5e14c5477c2b2024e698c47f52ede48d55563dadc46e783c06c24067aad1d8417cf7e2d8fec59444ea700cb6ded3dfb8379c73f39325c8a0ced75d62e995711542170784afba801ac92a14d95161fc109f220b6c120d4797155c3678ad0bbe529ce587250728b26fb64061a06a2ee67f8d1e3492b6a07d338d327ed709c42608bf2781279821cb1b21749203f6f6c9d04fff3a4ae", "derive": "2ed11775a07d4c1b517d354e5c89151b5336dab0588125e8f85990502ef6d85ef3309272cb31f4acb3a7fcf4942de8edc6f4329dbab4a1200d0facca24518edfe06f706e76a95b012e50a33e3f5ae3cdf41af17985317f19cccb7f753ea9bc57c30e0a7f1d0f7fd5234b5fcd2ed8a8ee1d60fd6b07af2b5ebfad1b5aef91c5f6be13d5f33981c9ef7f60a4ade34b9523a0d75bfb3a365e645a849caa98c12836dc0cc89fc2c4c322260a76c6ec499d39bb8ce8449eb3746a875356d5c27ac428513da2f9ae19424a"}, + {"seed": 91, "len": 4096, "xof": 16, "key": "0e8cbffd842407148829d0b971714b98068d3bc8afe1d04b5ebf2742245f5d1c", "ctx": "lambda-vm oracle review ctx 4096/16", "hash": "215da79fcb78be79dab730b78467a89d", "keyed": "17e84551f53d55feccf53cd8752232ab", "derive": "744b75aed4882da612ecabbeed555a24"}, + {"seed": 92, "len": 4096, "xof": 32, "key": "37d59cfec72fe64019e0139c588a3b2ba063e011e4b870cdb1ffe366582349af", "ctx": "lambda-vm oracle review ctx 4096/32", "hash": "2b2db219466f8183cd837cffb20505126a523114d0e2a69ddb425f9501933912", "keyed": "bd68a9f1b0d69d19bd1d0737004df16d3ff1ac827f80aef3804bb59f2d11961c", "derive": "1a485b0f2b1a6dcffa47e71159adceb8eec678d2576ee68d1dbde82c9a00d590"}, + {"seed": 93, "len": 4096, "xof": 64, "key": "cb00aa01b802dfc2eaa2256eed82a0208816629b4e905a9b3090c69e68274d03", "ctx": "lambda-vm oracle review ctx 4096/64", "hash": "0492d1ff2de887b4febea60dfa8ab27f9d322708de806629c2378282b3686518606927b089be53a118bde1205242973aa32eb3c32708cea83aecb0cb5ae0d08e", "keyed": "3939705bf049fecda37a7b3adfdfc277139ec2243353bccdafa11d7a841f662dd49379862bd294a7f3a04567bd9f9a5ba2785e7eff94fc16e5886eaf2439f4ab", "derive": "c71aa99c235917053a09e55bfda0ae9d5214f5faa6d54498f7ee690f6c6a2af0dae3578f9aef9ad1b4fb7af11656f97e95955af8d697b0570e41f18ef5c92e46"}, + {"seed": 94, "len": 4096, "xof": 131, "key": "8223c60b3ec3110cce7438b53f7223e9baa8c3abf2b43bffb5ce0df1b1118ca2", "ctx": "lambda-vm oracle review ctx 4096/131", "hash": "ad5d14700821d7ee7842e21815bde1f0f2c4374b57349b259b3d3de06337db474de14e8bf3d3315a5cec5cffd2a31c8a0a54150feda2b4f7dab19057a1e5d4ba13ddf1ad855968bf33cd77de4a46571f3b0c7869e5ec88ed34cbab8b4c18e2e3f64f736ca9cc06f145138de1ad586c2fbba5fd22cfd5d31c4fcbf8c3d71ab120ec552e", "keyed": "47765f1c959389a8a342f9050feb7d202d51f6c76e718dd9913289e00cac33d09e53dd6a9486f24fbe927dfa921c9e4060e5ec0587ab0eea545c998de6292fadc74ca5874629c9d2e0a85bd2d44dd0967bd6da74103f9b7062bbd3c0153b53f0342db20740e0e5065197a6c56362152649fd106662a2a3136f4bec163472633aedce57", "derive": "a966caccc348466a42226f6ed900ef7031c22eb751422ebc32c904e44dcbf5f5e3dd676a6cc16bbdfd6a87bd91d46c867bd850a864d2eb39be3f946a90b0ece4527838452b18d041b86b5e3b726905845f664afb0fed035d51dfdcb8e907b9a75ba216acd7f2182e34e5b47f65fc1977796f5a2132f4b6bcbc691bc9e7fae805c28287"}, + {"seed": 95, "len": 4096, "xof": 200, "key": "174e6662d2c05955c063523f5288eadb34328c4fa2b27165858897cae98b048d", "ctx": "lambda-vm oracle review ctx 4096/200", "hash": "05420df742c2a977945cd2090e7faf6d969e63a2c6238cc0770e40ac9a86669fe7ff09b94c0b2ceb41ae5ff5321864c1247f5bd42040ab4f02c49330807b307e30191bc6ae242805ff2512d417c847b12fb9d85555052ec43db978585716de1b6246b170d82cb92ac1fdef0b34818b4c06739152d25009f284eda20e493736fa55c688c9e6d6407c648dff4e72caad3b7f51aac4aec4c53b831b785006f0a1338212600b2ef65c3af11fcd729409bca76308a41da0639b6e23737ce70c86426fcc1beeed41559d7a", "keyed": "d9997ec1c3f46309869da8818b3e5f634a48adf8a2227dc3e2eeed9f9bd4b1842e442e1d444a2047e711de7c97bfd1b368f73c0157843e35ee4d46f4662d5eb259765ca7c4d50e57a7b93cbc7e2f18a085b083315782253a60049b5a1e5aafaa7f425ff81afc5f306ecd7306db3060d16f7df63e5dd81ea43fbee15f8c6d2198c1a001a576538bf22e489d1d812604c46e2dd6cfc405bb5241ac80b3dd5c0ef9e778daa460c3ad084bec4889c6a0009d200b765e131074781a0b247cde5700fc04d46a6fc1b019b6", "derive": "ec2762b33af35e89aa4c07c733054536582a31fb630bdd6fd8c25aed6ba6b36f1e66f577cc2458342d87c56474f9abeb6c81d2666fa1e9342d01abc12e35f1175395247bd1e654d74797bbd934424200de4f735307cc79fb0d640111559771911f0adb0ee081880b6a72c3281f6c2827453ee54c4cdd64fd419237353a1da8e6a08046c26b833407830a767febcc65e500e90195e13601285b4109d54c0d94eb55f15f3846b2b92dcc2e9ddc6d7f0232e50c151db38fd8495882764456e15893237c77345d5d95db"}, + {"seed": 96, "len": 4097, "xof": 16, "key": "69567c30361e6b67e55967d288477bf78c8e0af84fcda6abc9258567e858c90b", "ctx": "lambda-vm oracle review ctx 4097/16", "hash": "867aca04bbac9c1d3a07278138e9ad1f", "keyed": "38e03b39b3e55beb53bdd77ce350c832", "derive": "4753c2c7254782c32be47b7a410f4572"}, + {"seed": 97, "len": 4097, "xof": 32, "key": "8f930a77102fa62bb15ffc8cce6ff1314dcc31eb20f2d17050b09fc6602f520d", "ctx": "lambda-vm oracle review ctx 4097/32", "hash": "ed871b6a5ee95e25f03e981e5bec7758ec00523f4986852510db5a4162961c85", "keyed": "9d5efe1b892cbe1ae7ab8a04f0f56fd4882d0477a34a655b48218fae1c83165a", "derive": "3548c2cee59bbc4fdc4266169ddf3464dfdded393b0066bbe7a33490dd1107ad"}, + {"seed": 98, "len": 4097, "xof": 64, "key": "927ff7293c0dd8b741106fbb7479028a1fce6b6fefa6f6266296e989358c56e5", "ctx": "lambda-vm oracle review ctx 4097/64", "hash": "b6e61602cee007c2cc998ef402a2a50d02cc1b988dfbe7362139f06226f533e8f9556a1ec8b0ae749c7761da36cb5a97663eae8a13b13c7fd4ba39140ea12e1f", "keyed": "38390cedeb4636357acffbb91d85265c1d32f872c55e5e9e341e8f581df4e23e8823fa7cb5e239c857368bdd2c21984398d3662393910e2612f5c2ab1fe61780", "derive": "0f4b6becfee37bc9612d188bc1d80b5ff3f81396ada402e0daa5dc899e9e55c6cf2dc5cc759e77e0ffaa7eb26ec5785b1d589b9ddb14fab4963002670452194f"}, + {"seed": 99, "len": 4097, "xof": 131, "key": "b8bc393f6cd619e9b5748cc48c90ca3bdc39bf0ab4d2d43c502a939bdfbfbd56", "ctx": "lambda-vm oracle review ctx 4097/131", "hash": "c5e5938ad2e8304bf0d5bda578ddc94dfedc903f9d6cc4401b8b443ebb8cb6cf21ae9e8c453a5ea7d1a6e50c04995bbeb9bcedf458d0581d9c0ed2845d1c9bd9163da1e26478341d619b4aa441211ebad0344f44ad621d211e9b6295c9d74a121707802ec788a8f453c090cb6da6e95be787afa0b1c3efb8a82a0badb816919bf7be19", "keyed": "def95aa4c1ff157d04ff55cbd23310031603969f5b45decd791bb0ff5064643b6a31a69a16de293af9a7fe719c60cc5e3bba6c8bf4ebb04705da1ef8caba76635f84dffca60164493bb5beaec75224ec7a4b0818d4ad88ba3b8efdb70b52d10e66418757457e667b9e3239b3a5dbb8c6dd823dc93e0f3d0b7ee68736e010ecb271bafe", "derive": "68ae58a3f9156bf80acbc356843f6eff735adb807ef7fca62f034d02e85111f26b043baac74a36dda7531c8d24406dde16827c0e0dec91ec969d980e09ee2582b8e1b4f693efee008e9ff2f28fe8cab0b821b1a4dab4fb24813c09dccc3d195e3af888f9bb6e3487b53b3607913e9381e83e9c66784a7e1586b46e83681a9f8d2903e3"}, + {"seed": 100, "len": 4097, "xof": 200, "key": "4926ffd129f4d4da38a0464c3cd5ae104e540c96a195dd522573610ae621345a", "ctx": "lambda-vm oracle review ctx 4097/200", "hash": "b4b19f35b3eab71e25b86a0d12f234756d79531e32ea6b9ad70c9f275f949353ac8e6c18fbe4124bc16a572fdd3c1f89243eb10c5e0e86b4d344148513c3f802a8544d87bcc9aefa6a318602cce20fcc486fe7f265769cd4154a7b978f6e1d76946927f67af6967aa4906f74d09047fbdf203379d6f9eadff3612ac884b6839852a79716bc4bce6f7853325202b0fcf23573f41af4f8de558f9e62eb5268dc8712f7786194118f81694cdd73bc4d5153ae6c32fab2f35d5caf5f9c8d6ac0ee4ad0458f56c132efa4", "keyed": "ffdb15a8f97a8e8f27d1e6fa9f83b862bce80f164f894aa56a330e5366db864787fe52d1ec92af5ff062bb1f12564e8c004be44e59ae8734b131d609da13d6b915b1fb4c954afda7ad5ab0bcc086f203c03e05232d2be549ef0c73243418f249e31d68c2a022bd6a140402c444514213e8060df9d96d50ab7e10bc40e2dcae2506d4fa910e3639102967b8671489589b156068a28089dc5d278deaf7045ab64ff84c237245fd98f8951a5b17fe7de46a48eca62f6862cf986fa808b4ab8a3a0fbe3c65d49423574a", "derive": "c9bccf719c967863b07ccaaab3c44c0644af36192e6f7091680ffc5d2a36b4847082068e42de15377d55b4dd06ce66e60294168989306f12183933cb1e86289aadf85d7e09e3b6b71264be605b1d7774fdc4741c3b569362d2e158088430c1985a8eb41dde271c4d53eac15b4268794c2bfa46032d2f598e0d02f897b9767379b988466ecfbb7eecd9b9621c73ddc78e7017f63189aa06441594adf632bfd439ec66f98ab49d2591e0fdc906e7006ff057f149430f756ee06ab323506921f9552a475bf61d5ae158"}, + {"seed": 101, "len": 10000, "xof": 16, "key": "debf6bda8ae84c02678eb007feec1ae5037fe2fdd886c749bbd8ba07fa751ae1", "ctx": "lambda-vm oracle review ctx 10000/16", "hash": "298af4e1235a1a2cc4b1f7e6371e4296", "keyed": "985164b642278a61f7885477fcdd7d4a", "derive": "9edd4f3905cba501cf38174df53cfa57"}, + {"seed": 102, "len": 10000, "xof": 32, "key": "724ee3cb2af83fd3c7fe1479201facaef25de4d47ab374b04d51dc2a7dd2414c", "ctx": "lambda-vm oracle review ctx 10000/32", "hash": "8797cc68e0e8f52c95e0e1c805d3ed9b66ccee9b629f9c0ad014faaebd96877b", "keyed": "08bfaeb67847f9d8da30caf3daad2c4dd763013166dd954e33e001c5fcb38543", "derive": "195f9d3c9d30f5dec592cc0a81ed4d71c5bfe2b647ba8b2dbe48877e6b8ecc56"}, + {"seed": 103, "len": 10000, "xof": 64, "key": "07e8bda29e5416f49e9edd67d44e6a6eb3604796baa70a54c03d6db1b53a3246", "ctx": "lambda-vm oracle review ctx 10000/64", "hash": "691f34c88445969efaa89be227fd360393e3b9fd66075583c7214d3a22685894b5d5779035a9b3114de3e8c88c48d12f28165cf3b921f933e9758116f46b2cc6", "keyed": "8736e42e82ac72ad5296420d9b8ebdc1f7f16be14ee45f0f98c8bd608ad9dd878ed5a0794997ddf234b57a44eb56233bae99087f5de09bcab7b7256865fe77c6", "derive": "186867f6ee0a0a30cf90e931c687519c5c6015db1855bec7f687c0d14408f3f2f6170bb892a8104814730b5616eae4185abeb2184e4805c84304ff633871da89"}, + {"seed": 104, "len": 10000, "xof": 131, "key": "7cd1fd085fe8d82385a78adc0c2fb6967c37bceaf58c1ca36f75ccf94597297d", "ctx": "lambda-vm oracle review ctx 10000/131", "hash": "0a31dc0885cb9f01f608c472c927642bfc6da83faaeb3cbd08df693fa970e32fe63752f15ad7b44acc7aa84184f57f230256307e0e19d41bd7045b6e0acd2e0f1030d59fce33e6ec690eb1d61123b629f661af35f5b5289599a8c610e91954dfcfe2e10c51e989d2afac0f137db9cd9ed349bc9c268ecdb4de985890a780f9b4583491", "keyed": "789727759c1fa74d67fb10747ea2d6b63f880fa2c65384c7001edaa7a036c6a20e517f8722f41cba69eae3250ba124c6d5223382c7d7e715d12896091d4e06836afa08fb946a6e4095caed92fa1b32bf184170e17bd993664b243e25b4bad23b1fd502903ef89df669f42a33d3b8ecc0ae6e34c1e36d0fc30a205fca676b546bacdc8b", "derive": "6d23542e2401bdde33d6cfb6e1d5e711012e509a4d13075fc42b0f1f263c81807285351fe8fce9e755d45ebdaf1abec13e3031102472b775fde583672313104fbda0636b15485f925ce8a2a2bae14c4d7d9e4a595c1733c5e5017d9a54b67fa8238e43079b9ad609c0cc27c0bedf6bad841f8bf9f37cec048d6f99244df1e0c1e8bec2"}, + {"seed": 105, "len": 10000, "xof": 200, "key": "a20d1c6d3a8c8000132928d6d8de2d7a01b4352deb394230b46fe47a9d6d2319", "ctx": "lambda-vm oracle review ctx 10000/200", "hash": "de90c25b7c0cee636412502de2183d0f0ddac7238f059d540335550a92507c51181c3f525fe2072f17208a06e035a6b090c67184f792915181ff35b6986008ec6868104328fcc6f451da8478998ac6e310e4ce86f1a4356f839da5d21c44753a15486730c13028dd542d70ccbd220e32b4958e93845b6886937e7e1d2d720c3cb748876318bc9aecfcd7b7c2d66af44abe5757db8f0bc8eb697c1b6b4844d95569e295128700cfe2567def2ea7b13bcb53c315d020d9d9cf4abd77e4e242c16533fca443eb3aeeda", "keyed": "d5322c8131d3f7490f9b61b92ace6f127e372344af6acd440109be79ba3f3bbc0c9e7c68d251f1f6ba23b93d4f3c1e2bab8fbe4a6fabaabe17a27f12424117db0f4d24ca91ced9744601c9eea0083f0d0fa234af5ddd38bd316971f12570654c6871fb53edff0b2d2cc268a5898782b4e22c392d4b933e4f9120667caecb8c5f78bbc15ab143a97d13841dfebc3b7097569180a12b6d02350892625fab5bb723aa170c42da7f6213c005e76e82354ebcc2aa4074020000bb14a94035835748675a9ad1dac4d21abc", "derive": "49a119c6297ac0cfd9282cec209071d6b269b77b20cf1f32d700bb29d0174e65bd8149052f31efd0a6b1f9c902a6e8994ab821a84454877c412a7c8830219c8dd6dc0143accbe984687600e7e3b73f8c18929e98f434e8d151bf8ef849f3c48605d840a29065391ad342dcbd267e046d3558254f443eb539bf5878eb71721fed08922869cc7ab9d21218ca393dc300e970d0f7c0dcb15953639639c1ba29ffd6b411a297dd3288476d11c832cd326636d67ccacf258b48658877dc874594ee33163abf82196c02ce"}, + {"seed": 106, "len": 65536, "xof": 16, "key": "c7fcd83f19782a915cf9d246aec8c8ba5356c0236f5aab21745ca8706cd378e5", "ctx": "lambda-vm oracle review ctx 65536/16", "hash": "250e73c83ef8c2547bc66cbb62db7cf9", "keyed": "68a9254c8bca25432f39914236af4697", "derive": "c2610f8c15910689cc8b9071b3e618b6"}, + {"seed": 107, "len": 65536, "xof": 32, "key": "ed3888f753dca5e6740f4e89c20c13885e463030c76298c6c0cad9343166d9ea", "ctx": "lambda-vm oracle review ctx 65536/32", "hash": "d987ec830797a4b3c39f7b2da883106acde4dca8088a0c61c9ba75efb3db9f88", "keyed": "34b1c21894a95a245a74e0681ed5b38774e0b282adc477bdd1c58f3f5898f75b", "derive": "95b688c7d8a773cd6ed197bdfe9c3ddfcb3983c4b8a0aabaa36e1ffa6f21ad95"}, + {"seed": 108, "len": 65536, "xof": 64, "key": "5ca03baa8317d67cc39f60d104b35d1aef8869c74baccea4341bd825d246e351", "ctx": "lambda-vm oracle review ctx 65536/64", "hash": "32b03bba7802ca730ca05939ff1e1ffdec0a09af285130a4cdc148ef81d78eb98baa097a8ffad1e4b876f0bb59a18c7e2b45c8cb3813898e2403984f7a6c6071", "keyed": "3b0a32ca1166401b8d712f829dc16688095153d108912de3c8414e0a6750d6d0497d72fe25247c8d7e0e6e9c9023880cf0752310a7584748dc31891bfd9ef33c", "derive": "82899fe0ac33c8eaabc4b08dc0094cb450d9ccf0aebdda852eaab18adc4ad343d8d638e331496ef18fc23dfa48e3ce3cf77b3ad76403353d244b48e617b44da6"}, + {"seed": 109, "len": 65536, "xof": 131, "key": "f03938cf2f49ac7e99fdc4a957b6b3e99cc5288c4feacc5837526a08f35f5ad4", "ctx": "lambda-vm oracle review ctx 65536/131", "hash": "5ec02bd02ee4d382db8d10d0c4a2a014087558869944c60b97531eee05c2a5ee5fb8664fbf041bb64f09c5e997bd8a6db7dc18ecdf22285fe7a1ad368df8b0683e37c328cece89b0e90992bafc70f7987bb064338dc4cb025cf366aaee42c7e8fbcb9fe28a816b97c1766c7184bbdea668fe11db1f6aab31f189b28f9f4014a24b17b6", "keyed": "21c7d240682985db03161379efff7c36d04594c5a5b166b345a54a35f6fbb6c0c60a202e34a71ac97fb7f9888c86dfe66ef0781888245c332a45c7ff494385f6e7c950a1a561ab98dbb352ce4f9ea6fabb1684ac152ef00cb7d03f6811f7faf424e0dd4cb24de541a2a4762be4d2f76c22f752d18942a7c8961d0c99221da20a0b81a9", "derive": "f1c0de3ac81f9e3907a67c39b1a09949da0ff0561cfd84c6878cc7b969ee7cb89949bd0a4d217c7780f038cf7659a2e8e9f8472d9f414a3870e5adfecd48ecfe0187225bd5acffd898b25cbf7faeaa3afce321b2add22e62459e907f99843fd689ecc0e448c02b4a389db38c8fc52ca0f978e727077ec4dc58e5a6521ef07ad59a3c5a"}, + {"seed": 110, "len": 65536, "xof": 200, "key": "a7cbf3e166bbab1632153b7b7a9053d6b42f4d853b55e21867323ad5b55fcb0f", "ctx": "lambda-vm oracle review ctx 65536/200", "hash": "9d3608bf4225fd301cb08dbb27d22bdcd902d833ee5e8896e535d799c2ec10129c8aab47a6602262ad17b3589b66ee97047f29ab1606de811d00e3faa6bbfefc54afef9dbd61ca79675d3d583a819fae4e8adc0307da8e5e3202e2b25cc78af20e13d73a16656edb5ade79f77163d748813776916247f627452123b2aa46910a7fb7706460e8f72aa5bcb6f2685819960e6c85ce66ee560daf963c3afbe4a21e8fcf5750fcac93f62f13bd273d02082aa462f03497e97d0fca5a1b39c8a6fff4bd83aed05525c5e1", "keyed": "9b312955f025b12c33f0fb6d1a7724847b6bc5416bc8a7545b5b1c40e95047c479157113b57c0a4cc2d1acc5f16643f22285499f06f211c4ba208d9c2969e3a2bf21ccb3b1808ce309bb887eea150047a0c4e7ea981a57f7fbffbcf596ab616865fb3bdf428e77d1ec60e52a1f7f2f4796d13edfcc793ff7a5973793537793a902f315058d0c3f0a67968ff812657d46585c063165ff311c7ae19f218dde369cd5e89885ba08d2f4d5bb96c00ebc3e33dbda71fa8a873cbdd2c26b102aa49248fa4177f958fe5ce1", "derive": "8772e113081fe301b461adf0a1fbf105a199eee3d0c0f27836662a6ed7c62c01037ff56c18b298028babcb45aa1c7f13335f9fb8dc2f5931021be53ef21da4727b20927d3860eb17b3dd7ee51aeaae18aea989de6b897baa14547cb3e6c1bbec9bc63e66a078cac93815c4646e5690fb7ebbeabe010e44c9dd59eb2b5e6399b24c66f9d859390e170e94e45fe942d230788b9abce7392a3efe7d9652ca5e94cc63996cb47260dd9111864415368f206513609d744a807e7a2fd044b627d1ecc9b317bee514aaa7f4"}, + {"seed": 111, "len": 100000, "xof": 16, "key": "3b643c5a1b5e239a0799eaf8ff13931b82edb8fea42a47caa825b0244d4b7b00", "ctx": "lambda-vm oracle review ctx 100000/16", "hash": "dafc038f963a4fbb56a970d51433e9c7", "keyed": "ff1fe75b79c283887239712536a6f8b5", "derive": "4ca500d55e5ceec2fe5c02021403c4b5"}, + {"seed": 112, "len": 100000, "xof": 32, "key": "d394a71b890df7dfee174b95ed22722c143195fef201d138f2a8df894d16f144", "ctx": "lambda-vm oracle review ctx 100000/32", "hash": "58d738cba5b0b79e4d6f2035fb41acf271f1fed88b8e432a51958f43e827a212", "keyed": "6dd9792a242527b2d3013515dc5a7c75e36ddb6df6a6d41b373533f753c09454", "derive": "82f5c674b0ae22e41dd9424c64f66d4ff4ca3bc67ab20f0fcee1a97dfb6901f8"}, + {"seed": 113, "len": 100000, "xof": 64, "key": "f93f8bd6a30919f2178dd9860e65cc1da2d5a23e6dfda14cb14a2aa8e181732e", "ctx": "lambda-vm oracle review ctx 100000/64", "hash": "bf0c93ac003d8e92e511fe59d3309141fc1bc993c38d9a63737e4f508d68040f9422080ad44bb74dcb578d37bafb82ab021f758e98d347fe407576bce315f86d", "keyed": "90f3212d27f2a1919d209a19754424ff0b2704d7480d84ed882cb5fad926e7229d8d92a2b8daf1bc7b17e0333098265e7caf0eae401e0cbfcb898421a9245553", "derive": "7829eeda7bd48bb51b4b28147245a7bafb2df37e018029598098c99797cb174d2526858c341bf54a8d90eae6240572e0e590653c6486c5413bcbbdd390af9a25"}, + {"seed": 114, "len": 100000, "xof": 131, "key": "fcbd4502b4c333ae50c954a138f4b41913588e1d5e966ab5580f9d3c98d5bf28", "ctx": "lambda-vm oracle review ctx 100000/131", "hash": "4d1c5f62bf411a2b6f6711f4d9913c0669cc472b231099c41c7f9a45343ec421ac727a010b1e495a4311defea2baa88141a9dd60192a4d97a06f4818a527b4b65c322b4554b4a0c29fdeacd4fec1ba1afa2e0abec8b577c84f9417c376b2472272ac055878b0d42e5f9c4dd4939f27a4b46c0ef080c2e2abefe3fad3abb728c067cde5", "keyed": "bfa9973a8c02b8882e1450dd434a4b9d7072c877d8a2f94346fe06dbd5abd96d8fee4ed82aac7f3fec8addd2d7b197f030ea5322dc7d4217815a0a1f73987818ea935d09fa8f1513a8c18d87f9ba5f0f6a7fb57ab90c01c6aa8fa5aa59b304ae1240d0bcdd3e670786033ca4137ab6968a3491e2ebcddd638c633bbce0488b54eea92d", "derive": "a64ef45282602fb582847effffdddf218568cf916d38bab6cf711e8ec7569cb54d1ae624928ef8476942c2958c9d021e72ac012bd5c28cbbbfacf2239d968e783b310f23f6301eed08402d08078bda9da21a46c41c9728053e97b6d964f55aa3c8cfbb8698d6ef94dd2507ead85e454947d8bbea81159162c16c25134c0c92b519c0a7"}, + {"seed": 115, "len": 100000, "xof": 200, "key": "2268978ce6bb8db3b5133d660df23099096d6778ee1946a59d217165c34429a4", "ctx": "lambda-vm oracle review ctx 100000/200", "hash": "9d0a9882067f0f7e8adab6330d9e62498da7623daa31f39e984189b42830d444a4c7ad037fbe55db999626c6ccafb1737145a2ce28c63fa0380322bed696938395f8e18cf7c1ec1d032a81f044e4bd58a2f8b930b9f91fab466dcdbb97825ce61b696c1d9af7653380ca80c4ad3224338b2bb42d420c7d44d2d42e2beb9bd6e5d3f714df6dcd651c2ca655601c3ff2d50020df05b9f8c60617633f6f0648d4f57d1b98b9ff861dec6e0e3ee494729489d7b81f587fd0192fb7d93e25e28ca359967f6ce919f1ffa3", "keyed": "e6c5d4fb218f27ac862338bc0a6a272336d53ef38d415584fefe7989807ee222a683a3222031a9839dd5f02cd61687e9c6bd68792308dd86e43b3daf159defc21ca8f7663d4d8ce2c3c80539f9b9eae2656a4b299ca44d94381be56720599b799fef8e674e83f8114ac14f180deb55bdc0e3527fc2b69492540aa28dcf84f56ba9dcc89225122a0f7918b8892ee3afe40b24335ddb84a2f92796819602d8c88085cb77f45228cd7ed23d5b323c1bc7601fa85ceb1f9d57e541b834633ea7f5af70d52fcb3cc1fe4c", "derive": "1e6b77b3cfd3bb00000c04a2575b772c18720102877b8cc95379163219e67e03f3881cb7e78b893a11f19254cacf00302d68f8032999d7352f132341bb3a58eb002dfbc27dbd09694c82a8f34d006d1479508184954d36f58c821cfb7ae6965f4cce34037777213a433705a76390f33f6e5c80c4276dbdc1514a7e26437c5f490d5ec20b429016bb79d6eedf9b244694f6bd35cdc214fc27662b3e34bcc5ddee99c89fd4e4c443ad5f52958f64a4bc3514a4fb3b6bb4ec3e738836b2e03f0f096b98fdd9f91d1a43"} + ], + "known": {"empty": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", "abc": "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"} +} diff --git a/thoughts/blake3/ground-truth/Cargo.toml b/thoughts/blake3/ground-truth/Cargo.toml new file mode 100644 index 000000000..4e8de39f4 --- /dev/null +++ b/thoughts/blake3/ground-truth/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gt" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake3 = { version = "1.8.5", default-features = false, features = ["std", "pure"] } + +[profile.dev] +debug = false diff --git a/thoughts/blake3/ground-truth/src/main.rs b/thoughts/blake3/ground-truth/src/main.rs new file mode 100644 index 000000000..e79a10660 --- /dev/null +++ b/thoughts/blake3/ground-truth/src/main.rs @@ -0,0 +1,138 @@ +// Ground-truth BLAKE3 vector generator using the OFFICIAL blake3 crate (v1.8.5, +// pure-Rust feature, built offline from the local cargo registry). +// Emits JSON on stdout in the same shape as the upstream test_vectors.json, +// plus a randomised differential set. + +use blake3::Hasher; +use std::io::Write; + +const KEY: &[u8; 32] = b"whats the Elvish word for friend"; +const CONTEXT: &str = "BLAKE3 2019-12-27 16:29:52 test vectors context"; +const XOF_LEN: usize = 131; + +fn pattern_input(n: usize) -> Vec { + (0..n).map(|i| (i % 251) as u8).collect() +} + +fn hash_hex(input: &[u8], out_len: usize) -> String { + let mut h = Hasher::new(); + h.update(input); + let mut out = vec![0u8; out_len]; + h.finalize_xof().fill(&mut out); + hex(&out) +} + +fn keyed_hex(key: &[u8; 32], input: &[u8], out_len: usize) -> String { + let mut h = Hasher::new_keyed(key); + h.update(input); + let mut out = vec![0u8; out_len]; + h.finalize_xof().fill(&mut out); + hex(&out) +} + +fn derive_hex(ctx: &str, input: &[u8], out_len: usize) -> String { + let mut h = Hasher::new_derive_key(ctx); + h.update(input); + let mut out = vec![0u8; out_len]; + h.finalize_xof().fill(&mut out); + hex(&out) +} + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +// xorshift64* — deterministic, self-contained RNG so the Python side can +// reproduce the exact same inputs without sharing any code. +struct Rng(u64); +impl Rng { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545F4914F6CDD1D) + } + fn byte(&mut self) -> u8 { + (self.next_u64() >> 33) as u8 + } +} + +fn main() { + let lengths: Vec = vec![ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 63, 64, 65, 127, 128, 129, 1023, 1024, 1025, 2048, 2049, 3072, + 3073, 4096, 4097, 5120, 5121, 6144, 6145, 7168, 7169, 8192, 8193, 16384, 31744, 102400, + ]; + + let stdout = std::io::stdout(); + let mut w = std::io::BufWriter::new(stdout.lock()); + + writeln!(w, "{{").unwrap(); + writeln!(w, " \"key\": \"{}\",", String::from_utf8_lossy(KEY)).unwrap(); + writeln!(w, " \"context_string\": \"{}\",", CONTEXT).unwrap(); + writeln!(w, " \"cases\": [").unwrap(); + for (i, &n) in lengths.iter().enumerate() { + let inp = pattern_input(n); + writeln!(w, " {{").unwrap(); + writeln!(w, " \"input_len\": {},", n).unwrap(); + writeln!(w, " \"hash\": \"{}\",", hash_hex(&inp, XOF_LEN)).unwrap(); + writeln!(w, " \"keyed_hash\": \"{}\",", keyed_hex(KEY, &inp, XOF_LEN)).unwrap(); + writeln!(w, " \"derive_key\": \"{}\"", derive_hex(CONTEXT, &inp, XOF_LEN)).unwrap(); + writeln!(w, " }}{}", if i + 1 == lengths.len() { "" } else { "," }).unwrap(); + } + writeln!(w, " ],").unwrap(); + + // Randomised differential set. Inputs are generated from a self-contained + // xorshift64* stream that the Python side re-implements independently. + writeln!(w, " \"random\": [").unwrap(); + let rlens: Vec = vec![ + 0, 1, 2, 31, 32, 33, 63, 64, 65, 127, 128, 129, 512, 1000, 1023, 1024, 1025, 2048, 4096, + 4097, 10000, 65536, 100000, + ]; + let xofs: Vec = vec![16, 32, 64, 131, 200]; + let mut seedctr: u64 = 1; + let mut first = true; + for &n in &rlens { + for &xl in &xofs { + let seed = seedctr; + seedctr += 1; + let mut rng = Rng(seed); + let msg: Vec = (0..n).map(|_| rng.byte()).collect(); + let mut krng = Rng(seed ^ 0xDEADBEEF); + let mut key = [0u8; 32]; + for b in key.iter_mut() { + *b = krng.byte(); + } + let ctx = format!("lambda-vm oracle review ctx {}/{}", n, xl); + if !first { + writeln!(w, ",").unwrap(); + } + first = false; + write!( + w, + " {{\"seed\": {}, \"len\": {}, \"xof\": {}, \"key\": \"{}\", \"ctx\": \"{}\", \"hash\": \"{}\", \"keyed\": \"{}\", \"derive\": \"{}\"}}", + seed, + n, + xl, + hex(&key), + ctx, + hash_hex(&msg, xl), + keyed_hex(&key, &msg, xl), + derive_hex(&ctx, &msg, xl) + ) + .unwrap(); + } + } + writeln!(w, "\n ],").unwrap(); + + // A couple of well-known digests, for a human sanity check. + writeln!( + w, + " \"known\": {{\"empty\": \"{}\", \"abc\": \"{}\"}}", + hash_hex(b"", 32), + hash_hex(b"abc", 32) + ) + .unwrap(); + writeln!(w, "}}").unwrap(); +} From 5d969f9924f13a65e98e18e797267f0623358395 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 29 Jul 2026 15:35:11 -0300 Subject: [PATCH 03/14] docs(blake3): record what two independent oracle reviews established MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reviews, different models, no coordination between them. Both reached the same verdict independently: NO discrepancy in the primitive. Recording what each pinned, and — more usefully — what neither covered. Settled that no anchor covers: - Counter split order at t >= 2^32: t_lo -> v[12], t_hi -> v[13], verified BEHAVIOURALLY against the official crate via two independent counter paths (OutputReader::set_position, hazmat::HasherExt::set_input_offset) over counters 0..2^47, 44/44. Negative control breaks 5 of 6 chunk cases; the sixth is counter=0, correctly invariant under the swap. Closes ORACLE.md's O5. - Message schedule count AND direction: iterating permute from the identity reproduces all seven rows of the crate's MSG_SCHEDULE. Three mutants caught; a fourth (permute after the last round) is provably a no-op, so the guard cannot hide an off-by-one in either direction. - compress does not mutate its arguments; incremental update() == one-shot. Four more harness defects beyond the two already recorded: - the missing-file FileNotFoundError CASCADES — it kills anchors 2 and 3 and the canonical-vector emitter, which is why the gate's positive controls were blocked on an unrelated download. That single coupling is what made this recovery look worse than it was. - test_6round_derivation's first assertion is a tautology: compress_6round's body IS compress(rounds=6). ORACLE.md §2.6 calls it the "Code-diff anchor"; it establishes nothing. - compress() defaults to rounds=7, so a 6-round caller that omits the kwarg silently gets 7. Trace generators must use compress_6round — this should be enforced, not conventional. - ORACLE.md §5's ratio is internally inconsistent (~1/6 by its own numbers, not the "1/4-1/3" its prose claims); superseded by DESIGN.md §6's derived ~1/15. And the honest gap, recorded as the next reviewer's target: both passes established that the ORACLE defines the right function, so the gate's UNSATs are about the right function. NOBODY has audited the gate's transcription of that oracle into constraints — only its constants block was spot-checked. The EC campaign's equivalent audit found three premises the gate asserted and never read, one hiding a working forgery, so this is not hypothetical. The dangerous direction is a model STRONGER than what it models: it yields UNSAT where the real object is forgeable, and no positive anchor can catch it, because honest inputs satisfy a correct model and an over-strong one equally well. --- thoughts/blake3/README.md | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/thoughts/blake3/README.md b/thoughts/blake3/README.md index 239993809..865661278 100644 --- a/thoughts/blake3/README.md +++ b/thoughts/blake3/README.md @@ -81,6 +81,24 @@ authors' reference implementation and is entirely independent of still labels it "Official test_vectors.json". Read it as "checked against the official reference implementation using the official vector parameters". +### What the two reviews pinned that no anchor covers + +- **Counter split order at `t ≥ 2^32` — confirmed.** `t_lo = t mod 2^32 → v[12]`, + `t_hi = t >> 32 → v[13]`. Verified *behaviourally* against the official crate + through two independent counter paths (`OutputReader::set_position` and + `hazmat::HasherExt::set_input_offset`), over counters 0 … 2^47 including + 2^32−1, 2^32, 2^32+1: **44/44**. Negative control — swapping the halves — + breaks 5 of 6 chunk cases, the sixth being `counter = 0`, correctly invariant. + This closes ORACLE.md's own open question O5. +- **Message schedule count *and direction*.** Iterating `permute` from the + identity reproduces **all seven rows** of the crate's precomputed + `MSG_SCHEDULE`. Three mutants (permute before round 0, skip the 0→1 permute, + inverse direction) are all caught. A fourth — permuting *after* the last + round — is provably a no-op, so the trailing-permute guard is an optimisation + and cannot hide an off-by-one either way. +- `compress` does not mutate its arguments; incremental `update()` equals the + one-shot path over 60 random split patterns. + ### Known harness defects (found by review, not yet fixed) 1. `main()` prints `VALIDATION STATUS: VALIDATED … anchored on official test @@ -90,6 +108,22 @@ official reference implementation using the official vector parameters". 2. `test_internal_consistency` carries a comment describing a feed-forward recomputation (*"recompute v to check"*) that **is not implemented** — it only checks output length and the CV prefix. +3. **The missing-file failure cascades.** The `FileNotFoundError` at + `test_oracle.py:282` kills anchors 2 and 3 *and the canonical-vector + emitter* — which is why the z3 gate's positive controls were blocked on an + unrelated download. That single coupling is what made this recovery look + worse than it was. Anchors should fail independently. +4. **`test_6round_derivation`'s first assertion is a tautology** — + `compress_6round`'s body *is* `compress(rounds=6)`. ORACLE.md §2.6 calls it + the "Code-diff anchor"; it establishes nothing. The differs-from-7r half is + real. +5. **Footgun for the Rust phase:** `compress(...)` defaults to `rounds=7`, so a + 6-round caller that omits the kwarg silently gets 7. Trace generators must + call `compress_6round` — make it mandatory rather than conventional. +6. ORACLE.md §5's closing ratio is internally inconsistent: ~5–6k cell-equiv + against 24×1480 = 35,520 is ≈1/6, not the "¼–⅓" its prose claims. Superseded + by DESIGN.md §6's ≈1/15 against a 77,000 baseline — which is the number that + was actually derived. ## Open findings against the DESIGN (review, 2026-07-29) @@ -112,6 +146,28 @@ official reference implementation using the official vector parameters". 3. **The 3-op add carry encoding is ambiguous** — `(c1,c2) = (1,0)` and `(0,1)` both encode carry 1. Harmless for soundness; noted so nobody "fixes" it. +## Still unaudited — where to send the next reviewer + +Two independent reviews established that **the oracle defines the right +function**, so the gate's UNSATs are about the right function. They did *not* +audit the step after that: **nobody has checked the z3 gate's transcription of +the oracle into constraints.** Only its constants block +(`z3_blake_verify.py:50-80`) was spot-checked, and it matches exactly. + +That is the highest-value next pass, and the EC campaign is the reason to take +it seriously: the equivalent audit there +(`thoughts/ec-recover-opt/gate/TRANSCRIPTION-AUDIT.md`) found three premises the +gate asserted about the chip and never read, one of them hiding a working +forgery. The dangerous direction is a model **stronger** than the thing it +models — it yields UNSAT where the real object is forgeable, and a positive +anchor cannot catch it, because honest inputs satisfy a correct model and an +over-strong one equally well. + +Also still thin: neither review verified the recovery is *byte-identical to the +original* — only that the artifact is correct BLAKE3, which is a different and +weaker claim; and the historical counts ("35/35×3", "92/92" against PyPI +v1.0.9) remain unreproduced as recorded. + ## If this is picked up again The blocking item is a **protocol decision, not an engineering one**: the gate From c7087be4196e465fda6d44602ac2f2b24c5a588b Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 29 Jul 2026 16:32:28 -0300 Subject: [PATCH 04/14] fix(blake3): close the bus-binding design hole and the harness's false banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from the review were recorded as "open" instead of being fixed. Fixing them; they were small. DESIGN — the input/output binding on the internal Blake3 bus is now MANDATORY. §1.1 defined a receive of (h, m, t, block_len, flags) and a separate send of out[0..16], both at multiplicity mu, and §3 listed TIMESTAMP_0/1 as "bus binding (internal variant may omit)". Omitting it is a real hole: with two compressions in a trace, row A can receive inputs_A and send out_B while row B does the reverse — every tuple appears exactly once on each side, so the bus BALANCES, and both callers read a result that is not the compression of their own input. keccak.rs, the chip this design copies its I/O idiom from, carries the timestamp in both halves of its internal bus precisely for this reason. §1.1 now states the requirement with the attack and the precedent, §3's "may omit" is gone, and it is item 10 of §7's soundness-critical list — together with the fact that the gate CANNOT catch a violation, because it models arithmetic and has no bus layer at all. Added item 11 for the same reason: MAIN 0 proves one G under free inputs, so a wrong column index in one of the 48 emitted instances is invisible to it; the concrete positive controls are what cover that. The third finding (the 3-op carry admits (1,0) and (0,1) for carry 1) is left alone on purpose — it does not admit a wrong sum, so it is a note, not a defect, and the README says so to stop someone "fixing" it into a bug. test_oracle.py — the banner no longer lies, and a missing fixture no longer cascades. It printed "VALIDATION STATUS: VALIDATED ... anchored on official test vectors + official PyPI package + Plonky3" unconditionally, including on runs where an anchor SKIPped: the status dict was written and never read. It now reports what actually ran (VALIDATED / PARTIALLY VALIDATED / NOT VALIDATED) and names the anchors it is NOT anchored on. Separately, one FileNotFoundError used to abort anchors 2 and 3 AND the canonical-vector emitter, which is what silently blocked the z3 gate's positive controls on an unrelated download; anchors are now independent and a missing fixture skips only itself. Anchor 1 is relabelled "Official-parameter vectors" and prints its provenance, because the shipped file is regenerated from the blake3 crate rather than downloaded — a genuine non-circular anchor, but not the published artifact. Both fixes verified to bite: with official_test_vectors.json removed, anchor 3 still runs, the canonical vectors are still emitted, and the banner degrades to PARTIALLY VALIDATED naming what it lost. Gate re-run after the changes: OVERALL PASS, positive controls included. --- thoughts/blake3/README.md | 81 ++++++++++++-------- thoughts/blake3/blake3-chip/DESIGN.md | 30 +++++++- thoughts/blake3/blake3-oracle/test_oracle.py | 61 ++++++++++++--- 3 files changed, 127 insertions(+), 45 deletions(-) diff --git a/thoughts/blake3/README.md b/thoughts/blake3/README.md index 865661278..7a30680c7 100644 --- a/thoughts/blake3/README.md +++ b/thoughts/blake3/README.md @@ -99,52 +99,69 @@ official reference implementation using the official vector parameters". - `compress` does not mutate its arguments; incremental `update()` equals the one-shot path over 60 random split patterns. -### Known harness defects (found by review, not yet fixed) - -1. `main()` prints `VALIDATION STATUS: VALIDATED … anchored on official test - vectors + official PyPI package + Plonky3` **even when anchor 2 SKIPs** — the - `status["external_anchor"]` dict is written and never consulted. Observed - firing. The per-anchor lines are honest; the summary overstates. -2. `test_internal_consistency` carries a comment describing a feed-forward +### Harness defects — FIXED + +1. ~~`main()` printed `VALIDATION STATUS: VALIDATED … anchored on official test + vectors + official PyPI package + Plonky3` **even when anchor 2 SKIPped**~~ — + the `status` dict was written and never read. **Fixed:** the banner now + reports what actually ran (`VALIDATED` / `PARTIALLY VALIDATED` / `NOT + VALIDATED`) and names the anchors it is *not* anchored on. Verified by + running with a fixture removed. +2. ~~The missing-file failure **cascaded**~~ — one `FileNotFoundError` killed + anchors 2 and 3 *and* the canonical-vector emitter, which is why the gate's + positive controls were blocked on an unrelated download. **Fixed:** anchors + are independent; a missing fixture SKIPs only itself. Verified — with + `official_test_vectors.json` removed, anchor 3 still runs and the vectors are + still emitted. +3. ~~Anchor 1 was labelled "Official test_vectors.json"~~ — it is regenerated + from the crate. **Fixed:** relabelled "Official-parameter vectors" and the + run prints its provenance. + +### Known harness defects — still open (low severity) + +4. `test_internal_consistency` carries a comment describing a feed-forward recomputation (*"recompute v to check"*) that **is not implemented** — it only checks output length and the CV prefix. -3. **The missing-file failure cascades.** The `FileNotFoundError` at - `test_oracle.py:282` kills anchors 2 and 3 *and the canonical-vector - emitter* — which is why the z3 gate's positive controls were blocked on an - unrelated download. That single coupling is what made this recovery look - worse than it was. Anchors should fail independently. -4. **`test_6round_derivation`'s first assertion is a tautology** — +5. **`test_6round_derivation`'s first assertion is a tautology** — `compress_6round`'s body *is* `compress(rounds=6)`. ORACLE.md §2.6 calls it the "Code-diff anchor"; it establishes nothing. The differs-from-7r half is real. -5. **Footgun for the Rust phase:** `compress(...)` defaults to `rounds=7`, so a +6. **Footgun for the Rust phase:** `compress(...)` defaults to `rounds=7`, so a 6-round caller that omits the kwarg silently gets 7. Trace generators must - call `compress_6round` — make it mandatory rather than conventional. -6. ORACLE.md §5's closing ratio is internally inconsistent: ~5–6k cell-equiv + call `compress_6round`. Left as-is deliberately: changing the validated + oracle's signature would invalidate the anchors it just passed. +7. ORACLE.md §5's closing ratio is internally inconsistent: ~5–6k cell-equiv against 24×1480 = 35,520 is ≈1/6, not the "¼–⅓" its prose claims. Superseded by DESIGN.md §6's ≈1/15 against a 77,000 baseline — which is the number that was actually derived. -## Open findings against the DESIGN (review, 2026-07-29) +## DESIGN findings (review, 2026-07-29) — FIXED IN THE DESIGN -1. **The internal `Blake3` bus has no input↔output binding.** §1.1 defines a +1. **The internal `Blake3` bus had no input↔output binding.** §1.1 defined a receive of `(h, m, t, block_len, flags)` and a separate send of `out[0..16]`, - both at multiplicity μ, and §3 lists `TIMESTAMP_0/1` as "bus binding - (internal variant **may omit**)". If omitted, with two compressions in a - trace a prover can have row A receive inputs_A and send out_B while row B - does the reverse: every tuple appears once on each side, **all buses - balance**, and both callers get the wrong answer. The design's own cited - precedent does not do this — keccak's internal bus carries - `TIMESTAMP_0, TIMESTAMP_1` in *both* halves (`keccak.rs`, send at round 0 / - receive at round 24). **Do not omit the timestamp.** The gate cannot catch - this: it models arithmetic only and has no bus layer. + both at multiplicity μ, while §3 listed `TIMESTAMP_0/1` as "bus binding + (internal variant **may omit**)". Omit it and, with two compressions in a + trace, row A can receive inputs_A and send out_B while row B does the + reverse: every tuple appears once on each side, **the bus balances**, and + both callers read a wrong result. The design's own cited precedent does not + do this — keccak carries `TIMESTAMP_0, TIMESTAMP_1` in *both* halves of its + internal bus (send at round 0, receive at round 24). + **Fixed:** §1.1 now states the binding is mandatory in both tuples, with the + attack and the keccak precedent spelled out; §3's "may omit" is gone; and it + is item 10 of §7's soundness-critical list. Also recorded there: **the gate + cannot catch a violation**, since it models arithmetic with no bus layer. 2. **"Covers every G, hence every round" is a model argument.** MAIN 0 proves - one G under free inputs; in Rust, 48 G instances are emitted separately and a - wrong column index in instance #37 is not covered by it. The concrete - positive controls do cover it — keep them runnable, and prefer `--full`'s - monolithic UNSAT before shipping. + one G under free inputs; in Rust the 48 instances are emitted separately, so + a wrong column index in instance #37 is invisible to it. + **Fixed:** now item 11 of §7, pointing at the concrete positive controls as + the thing that covers it and requiring `--full`'s monolithic UNSAT before + Rust ships. The controls themselves were unrunnable at review time and now + run and pass 4/4, so the residual risk is materially lower than when the + finding was written. 3. **The 3-op add carry encoding is ambiguous** — `(c1,c2) = (1,0)` and `(0,1)` - both encode carry 1. Harmless for soundness; noted so nobody "fixes" it. + both encode carry 1. Checked: it does not admit a wrong `s`, so this is a + note rather than a defect, recorded so nobody "fixes" it into a bug. **No + change made, deliberately.** ## Still unaudited — where to send the next reviewer diff --git a/thoughts/blake3/blake3-chip/DESIGN.md b/thoughts/blake3/blake3-chip/DESIGN.md index 2abae7bcb..8aec428d3 100644 --- a/thoughts/blake3/blake3-chip/DESIGN.md +++ b/thoughts/blake3/blake3-chip/DESIGN.md @@ -46,6 +46,23 @@ child chaining values as the message and reads back the truncated CV. call sites read `out[0:8]`; the chip always produces all 16 (the XOF root needs them, oracle §2.4). +**Both tuples MUST lead with `TIMESTAMP_0, TIMESTAMP_1` — this is mandatory, +not optional.** The receive and the send are two separate interactions, so +without a key present in *both* nothing ties a row's inputs to its own outputs. +With two compressions in a trace a prover could then have row A receive +`inputs_A` and send `out_B` while row B does the reverse: every tuple still +appears exactly once on each side, **so the bus balances**, and both callers +read a result that is not the compression of their own input. + +This is not a hypothetical hardening: `keccak.rs` — the chip this design copies +its I/O idiom from (§1.2) — carries `TIMESTAMP_0, TIMESTAMP_1` in *both* halves +of its internal `Keccak` bus (send at `round = 0`, receive at `round = 24`) +for exactly this reason. Do not deviate from it. + +**The gate cannot check this.** `z3_blake_verify.py` models arithmetic only and +has no bus-interaction layer at all, so a missing binding leaves every UNSAT on +the board unchanged. It has to be got right by construction. + `IV[0..4]` (v[8..11]) are **compile-time constants inlined** into the round-0 arithmetic — not columns, not on the bus. @@ -108,7 +125,7 @@ One row = one compression call. Names group by role; counts are for `ROUNDS=6`. | block | columns | count | notes | |---|---|---:|---| -| `TIMESTAMP_0/1` | 2 | 2 | bus binding (internal variant may omit) | +| `TIMESTAMP_0/1` | 2 | 2 | bus binding — **mandatory in both the receive and the send** (§1.1); omitting it lets two rows swap outputs with the bus still balancing | | `MU` | 1 | 1 | multiplicity / gate flag | | `H[0..8]` | 8 words | 32 | input CV bytes | | `M[0..16]` | 16 words | 64 | input message bytes | @@ -340,6 +357,17 @@ oracle's prose "¼–⅓ of a keccak permutation" is inconsistent with its own expressions stay `< 2^35 ≪ p`, so `≡0 mod p` ⇒ `=0` as integers; the whole soundness argument depends on operands being genuine ≤32-bit (byte columns) and carries being genuine bits. +10. **`TIMESTAMP_0/1` in BOTH the `Blake3` receive and send** (§1.1). Without a + key in both tuples nothing binds a row's inputs to its own outputs, and two + compressions can swap results while the bus still balances. `keccak.rs` does + this correctly and is the pattern to copy. **The gate cannot catch a + violation** — it models arithmetic only, with no bus layer — so this one is + on the implementer, not on a green board. +11. **Every G instance must be wired as MAIN 0 models it.** MAIN 0 proves *one* + G under free inputs; the 48 unrolled instances are emitted separately, so a + wrong column or message index in a single instance is invisible to it. The + concrete positive controls are what cover that — keep them runnable, and run + `--full`'s monolithic UNSAT before shipping Rust. --- diff --git a/thoughts/blake3/blake3-oracle/test_oracle.py b/thoughts/blake3/blake3-oracle/test_oracle.py index 48a617bec..24eccef98 100644 --- a/thoughts/blake3/blake3-oracle/test_oracle.py +++ b/thoughts/blake3/blake3-oracle/test_oracle.py @@ -276,24 +276,43 @@ def main(): print("BLAKE3 compression-function ORACLE — validation") print("=" * 74) - status = {"external_anchor": False} - - # Anchor 1 - checked, total, ctx = test_official_vectors() - print(f"[1] Official test_vectors.json : PASS ({checked}/{total} cases x 3 modes)") - print(f" modes: default hash, keyed hash, derive_key context={ctx!r}") - status["external_anchor"] = True + # Each anchor is independent: a missing fixture SKIPs that anchor only. It must + # never cascade — a FileNotFoundError here used to abort anchors 2 and 3 AND the + # canonical-vector emitter below, which silently blocked the z3 gate's positive + # controls on an unrelated download. + status = {} + + # Anchor 1. NOTE: the vector file ships regenerated from the official `blake3` + # Rust crate (see ../ground-truth/), not downloaded from upstream. Same official + # parameters and a non-circular reference, but not the published artifact — the + # label says so rather than claiming more than we have. + try: + checked, total, ctx = test_official_vectors() + print(f"[1] Official-parameter vectors : PASS ({checked}/{total} cases x 3 modes)") + print(f" modes: default hash, keyed hash, derive_key context={ctx!r}") + print(" source: regenerated from the official blake3 crate, not the published file") + status["official_vectors"] = "PASS" + except FileNotFoundError as e: + print(f"[1] Official-parameter vectors : SKIP (missing fixture: {os.path.basename(str(e.filename or e))})") + status["official_vectors"] = "SKIP" # Anchor 2 n2 = test_pypi_blake3() if n2 is None: print("[2] Official `blake3` PyPI pkg : SKIP (package not importable)") + status["pypi"] = "SKIP" else: print(f"[2] Official `blake3` PyPI pkg : PASS ({n2} randomised differential checks, 3 modes)") + status["pypi"] = "PASS" # Anchor 3 - n3 = test_plonky3_differential() - print(f"[3] Plonky3 blake3-air (direct): PASS ({n3} random compressions, flags=0)") + try: + n3 = test_plonky3_differential() + print(f"[3] Plonky3 blake3-air (direct): PASS ({n3} random compressions, flags=0)") + status["plonky3"] = "PASS" + except (FileNotFoundError, ImportError) as e: + print(f"[3] Plonky3 blake3-air (direct): SKIP ({e})") + status["plonky3"] = "SKIP" # Internal ni = test_internal_consistency() @@ -303,10 +322,28 @@ def main(): differ = test_6round_derivation() print(f"[4] 6-round variant derivation : PASS (=compress(rounds=6); differs from 7r on {differ}/2000)") + # The banner reports what actually ran. It previously printed "VALIDATED ... + # anchored on official test vectors + official PyPI package + Plonky3" + # unconditionally, including when anchors had SKIPped — the status dict was + # written and never read. Claiming an anchor you did not run is worse than + # running none. + passed = [k for k, v in status.items() if v == "PASS"] + skipped = [k for k, v in status.items() if v == "SKIP"] + label = { + "official_vectors": "official-parameter vectors", + "pypi": "official PyPI package", + "plonky3": "Plonky3 independent compression", + } print("=" * 74) - print("VALIDATION STATUS: VALIDATED") - print(" 7-round reference: anchored on official test vectors + official") - print(" PyPI package + Plonky3 independent compression.") + if not passed: + print("VALIDATION STATUS: NOT VALIDATED (no external anchor ran)") + elif skipped: + print("VALIDATION STATUS: PARTIALLY VALIDATED") + else: + print("VALIDATION STATUS: VALIDATED") + print(f" 7-round reference: anchored on {', '.join(label[k] for k in passed) or 'nothing'}.") + if skipped: + print(f" NOT anchored on : {', '.join(label[k] for k in skipped)} (skipped this run).") print(" 6-round variant : derivative anchor (loop-bound diff) + canonical vectors below.") print("=" * 74) From 8fec369e725929b8d443090db4d7c9223eda1571 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 29 Jul 2026 17:29:00 -0300 Subject: [PATCH 05/14] gate(blake3): two transcription audits, and the corrections they forced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent transcription audits of z3_blake_verify.py — one authored elsewhere (TRANSCRIPTION-AUDIT.md), one an executable 74-check suite (audit_gate_transcription.py + GATE-TRANSCRIPTION-AUDIT.md). Both ask the only dangerous question: is the gate's model STRONGER than the thing it models? A weaker model yields spurious SAT, which is safe; a stronger one yields UNSAT on a forgeable object, and no positive anchor can see that, because an honest witness satisfies a correct model and an over-strong one equally well. Headline: every equation in the gate matches the design it encodes, and the gate's bref_* reference is element-wise identical to the externally-anchored oracle (IV, MSG_PERMUTATION, G_CALLS, counter split across 2^32, and permute applications = max(rounds-1,0)). So the "gate proves the wrong function" risk is closed. The mutation sweep fires on 7/7 bug classes that ship no negative control, including a wrong column in G instance #7 — the per-instance wiring gap recorded as §7.11. One audit also ran anchor 2 live (PyPI, 92/92), so the historical counts now reproduce as recorded rather than on trust. THE FINDING, agreed by both and now written into DESIGN §7.4/§7.5: the "free range check" invariant is DECLARED, not derived. build_g returns each add output as fresh_word() = 4x BitVec(...,8), so byte range holds by construction in the model, and the gate proves the identical UNSAT for a chip that has the downstream XOR and for one that does not. Dropped, the sum is forgeable — a = b = 0x80000000, honest s = 0, forged s = 2^32 with carry = 0, satisfying every modelled constraint. Same blind spot for the message columns: m is declared as bytes, so the 32 explicit AreBytes sends §4.7 requires are unverifiable here, and without them a word has many F_p cell representations of one value. These invariants rest on the implementer; a green board is not evidence for them. Corrections forced into DESIGN.md: - §4.2/§7.3: the load-bearing bound set is AT LEAST ONE of {SLL_lo, SLL_hi} — the two SLLC bounds are not load-bearing at all, so only half the AreBytes sends per rotation carry soundness weight. The composed forgery with both SLL bounds dropped exists for exactly ONE input, X = 0xFFFFFFFF (forged Y = 0), exhaustive for r = 4 and r = 9 — the isolated control made it look reachable for arbitrary inputs. And the rotation OUTPUT needs no range check of its own; the recombine identities pin it. - §4.8: the recombine identity's body is linear, so 1 -> 2 after x mu, not the 2 -> 3 claimed. Overstated in the safe direction; "no constraint exceeds 3" is unaffected. - ground-truth/Cargo.toml needed an empty [workspace] table or cargo absorbs it into the parent workspace and the documented regeneration flow fails out of the box. That was a real bug in what I committed. Both audits independently verified the previous commit's fixes are real: the TIMESTAMP binding (with the keccak precedent confirmed at keccak.rs:264-319), the banner honesty fix, and the cascade fix — the last two exercised live by renaming the fixture away. Also committing the t >= 2^32 counter probe (ground-truth/src/bin/), and a .gitignore for target/, Cargo.lock, __pycache__ and the 114 MB venv. Re-verified after all edits: suite 74/74, gate OVERALL PASS. --- thoughts/blake3/.gitignore | 4 + thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md | 471 +++++++ thoughts/blake3/TRANSCRIPTION-AUDIT.md | 192 +++ thoughts/blake3/audit_gate_transcription.py | 1198 +++++++++++++++++ thoughts/blake3/blake3-chip/DESIGN.md | 40 +- thoughts/blake3/ground-truth/Cargo.toml | 2 + .../ground-truth/src/bin/counter_probe.rs | 32 + 7 files changed, 1936 insertions(+), 3 deletions(-) create mode 100644 thoughts/blake3/.gitignore create mode 100644 thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md create mode 100644 thoughts/blake3/TRANSCRIPTION-AUDIT.md create mode 100644 thoughts/blake3/audit_gate_transcription.py create mode 100644 thoughts/blake3/ground-truth/src/bin/counter_probe.rs diff --git a/thoughts/blake3/.gitignore b/thoughts/blake3/.gitignore new file mode 100644 index 000000000..7fcf8256c --- /dev/null +++ b/thoughts/blake3/.gitignore @@ -0,0 +1,4 @@ +target/ +Cargo.lock +__pycache__/ +venv/ diff --git a/thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md b/thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md new file mode 100644 index 000000000..5d8c7ba17 --- /dev/null +++ b/thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md @@ -0,0 +1,471 @@ +# Transcription audit — does the BLAKE3 gate assert more than the design delivers? + +Adversarial, one-directional audit of `blake3-chip/z3_blake_verify.py` against +(a) the oracle `blake3-oracle/blake3_ref.py` and (b) the constraint system +`blake3-chip/DESIGN.md` specifies. Branch `spike/blake3-recovered`. + +Only one direction is dangerous. A model **weaker** than the object yields a +spurious SAT — a false alarm. A model **stronger** yields UNSAT on an object +that is genuinely forgeable — false assurance — and no positive control can see +it, because an honest witness satisfies a correct model and an over-strong model +equally well. The three positive controls and the 35-vector oracle anchor are +blind to exactly this. + +Reproduce: `python3 audit_gate_transcription.py` (~4 min, 73 checks) or +`--slow` (+ the gate's own BV UNSATs, ~8 min). Every check is paired with a +tamper that must make it fail; a check that does not bite is itself reported as +a failure. No file outside this audit was modified — tampers are applied to +in-memory copies and reverted. + +--- + +## Verdict + +**One over-strong assertion, and it is the load-bearing one: the gate declares +byte-ness rather than deriving it.** Every committed cell in the model is a +`BitVec(…, 8)`, so the range check that DESIGN §4.3/§4.4/§5/§7.4 make the whole +soundness argument rest on is an *axiom of the model*, not something the model +can observe being present or absent. The gate proves the same UNSAT for the +designed chip and for a chip with **no range checks at all**. + +I checked whether that gap is live. It is not, today: I traced every SSA value +in `build_g` and `build_compress` mechanically and the design's premise holds — +all 288 add/shift outputs of a 6-round compression are consumed by a `ByteAlu` +XOR, for ROUNDS ∈ {1,2,6,7}. So the gate's UNSAT is *correct*, for a reason the +gate does not model. It would stay UNSAT under three specific deviations, one of +them invited by DESIGN §1.1 itself. + +Transcription (a) — `bref_*` vs the oracle — is **exact**, mechanically, on every +element the brief named. + +| # | severity | what | +|---|---|---| +| **F1** | **high** | **Model stronger.** Byte-ness is the `BitVec(…,8)` declaration. The gate cannot distinguish a chip that range-checks an add output from one that does not, nor a chip with §4.7's 32 message `AreBytes` sends from one without them. The premise holds today; nothing in the gate would notice it becoming false. | +| **F2** | medium | **Model weaker / evidence over-stated.** The width audit's `DROP SLL bound → SAT` control is run on a *single-halfword fragment*. Composed with the second identity, the recombine and the downstream byte check, dropping one `SLL` bound is **UNSAT** and dropping both is forgeable at **exactly one input**, `X = 0xFFFFFFFF`. §7.3's stated mechanism is also backwards. | +| **F3** | medium | The "covers every G, hence every round" chaining argument (§9, MAIN 0) **is** the free-range-check argument in disguise, and the gate models neither. It is prose on both counts. | +| F4 | low | μ, padding rows, the degree ledger, and the bus layer are not carried at all. Scope, not error — but §7.1's "μ-gate every eval constraint" and §4.8's degree table are cited as gate-backed and are not. | +| F5 | cosmetic | Three documentation defects (§7.3's mechanism, §4.8's recombine degree row, §3-vs-§4.3 on whether add2 commits a carry column). | + +Along the way, two results the gate did not have: + +* The rotation is pinned to `rotr12` / `rotr7` **for all 2³² inputs in the + field**, not just in BV and not just at one concrete halfword. +* The necessary *and sufficient* `AreBytes` set for a rotation is **one of + `{SLL_lo, SLL_hi}`**. The two `SLLC` bounds and one of the two `SLL` bounds + are not load-bearing — 3 of the 4 sends per rotation, i.e. 288 of the design's + 1,250 sends per compression. + +--- + +## F1 — the free range check is declared, not derived + +This is the claim DESIGN §7.4 flags as soundness-critical and the one the gate is +cited to discharge: + +> §4.3 `s`'s bytes are range-checked **free** by the next XOR that consumes `s` +> §5 Every committed add/shift word is an operand of a later XOR ⇒ its bytes +> are byte-range-checked for free by that `ByteAlu` lookup. +> §7.4 **Every add/shift output must actually feed a downstream XOR** (its only +> range check). If a future refactor reorders so an add output is *last* +> with no XOR consumer, add an explicit AreBytes or the carry argument is +> unsound. + +The model's answer to "where does `s ∈ [0,2^32)` come from" is +`z3_blake_verify.py:133-134`: + +```python +def fresh_word(self): + return [self._fresh(8) for _ in range(4)] +``` + +and `:128-129`, `self._fresh(w=8)` → `BitVec(f"{tag}_v{n}", w)`. `add2` +(`:176-184`) and `add3` (`:186-197`) both begin `s = self.fresh_word()`. So `s` +is four 8-bit bitvectors *by construction*. The audit confirms it mechanically: +every cell the model creates is width 8, and every constraint it emits is `=` +(xor / sum / shift / recombine) or `or` (carry booleanity) — 1,408 and 288 +respectively for a 6-round compression. **There is no range-check object in the +model to be present or absent.** + +**In fairness, the gate says it is doing this.** The `Circuit` class header +(`:118-119`) reads *"A `word` is a list of 4 free 8-bit BVs … Byte width == the +ByteAlu/AreBytes range-check contract"*, and the module docstring (`:18-21`) +lists `ByteAlu[XOR]` and `AreBytes` under "Chip contracts assumed". So this is a +**documented assumption, not a hidden one** — unlike the EC audit's F2, nothing +here is detected from a comment. Inside the class, "AreBytes" occurs only in +those two comments (`:119`, `:212`); the only place the gate *models* an +`AreBytes` bound is `field_shift_bound` (`:388`), and only for the isolated +fragment F2 is about. + +The problem is that the equation "byte width == the range-check contract" is +applied **uniformly to every column**, including the ones for which the design +must separately arrange the contract (§4.2's and §4.7's explicit sends, §5's +downstream-XOR argument) — and DESIGN §9 then cites the gate's UNSAT as +establishing what the comment assumed. The gate's own docstring (`:5`) states +the method as "Every committed column of the designed chip is a FREE bitvector"; +it is a free *byte*, and byte-ness is the property under proof. + +### Which gate rows this invalidates + +| DESIGN §9 row | as written | what it actually establishes | +|---|---|---| +| MAIN 0, one G, free inputs, **UNSAT** | "the quarter-round … is **correctly & tightly constrained**" | correctly & tightly constrained **given that its inputs and its add outputs are byte-range-checked** — which is §7.4/§7.5's obligation on the Rust, not a result | +| MAIN 1, rounds=0, **UNSAT** | "`v` layout … and the feed-forward are correct" | same caveat for `h`, `t_lo/t_hi`, `block_len`, `flags` | +| §9 "Proven (symbolic, all inputs): the G quarter-round … hence, by the chaining argument, the full N-round compression" | unconditional | conditional on the same premise — see F3 | + +### Where the missing range check actually bites + +The gate declares byte-ness on *every* cell, so I checked, per SSA class, +whether the range check is load-bearing at all. Three of the six classes do not +need one; the model's blanket declaration hides that distinction too. + +| SSA class | per G | model's source of byte-ness | chip's source (DESIGN) | is it load-bearing? | +|---|---:|---|---|---| +| `add2`/`add3` output `s` (A1, C1, A2, C2) | 4 words | `BitVec(…,8)` | downstream `ByteAlu` (§4.3/§4.4/§5) | **YES** — without it the chip may commit the **unreduced** sum | +| `ByteAlu` XOR output (X1..X4) | 4 words | `BitVec(…,8)` | the lookup itself | n/a — pinned by contract | +| `SLL_lo`, `SLL_hi` | 2 halfwords/rot | `BitVec(…,8)` | explicit `AreBytes` (§4.2) | **YES, but only one of the two** (F2) | +| `SLLC_lo`, `SLLC_hi` | 2 halfwords/rot | `BitVec(…,8)` | explicit `AreBytes` (§4.2) | **no** — proved | +| rotation output `Y` (B1, B2) | 1 word/rot | `BitVec(…,8)` | downstream `ByteAlu` (§5) | **no** — the two recombine identities pin its *value* even with free field cells | +| message `m` | 16 words | `BitVec(…,8)` | explicit `AreBytes`, §4.7 | **YES** for cell-binding | +| `h`, `t_lo/t_hi/block_len/flags` | 12 words | `BitVec(…,8)` | XOR consumers (§4.7) | for cell-binding | + +Each row is an executable check in `audit_gate_transcription.py` §B/§C. + +### The forgery it hides — construction + +**(1) An add output without its ByteAlu consumer.** This is verbatim the §7.4 +deviation. Model the design's add in the field instead of in BV: `s` is four +Goldilocks cells, `carry ∈ {0,1}`, and the only constraint is §4.3's sum +identity. + +``` +a = b = 0x80000000 +honest : carry = 1, s = 0 cells [0, 0, 0, 0] +forged : carry = 0, s = 2^32 cells [2^32, 0, 0, 0] +``` + +Both satisfy `a + b ≡ s + 2^32·carry (mod p)` and the booleanity. The add no +longer computes mod 2^32; the chip's `v[c]` is off by exactly 2^32, and the +error propagates through every downstream add. `audit_gate_transcription.py` §C +runs it and reports both `add2` and `add3` FORGEABLE with the range check +removed, PINNED with it, symbolically over all operands. The BV model reports +UNSAT in both worlds because there is nothing to remove. + +**(2) §4.7's message range check — the one explicit input `AreBytes` in the whole +design (32 sends).** The gate models `m = [cir.fresh_word() for _ in range(16)]` +(`:332`), i.e. 64 declared bytes. Drop the sends and the 64 cells are free field +elements entering only through `wval(M) = Σ m_i·2^{8i}`, so the chip binds the +*value*, not the bytes: + +``` +m0 honest : [0x9A, 0x00, 0x13, 0x7F] +m0 forged : [0x19A, p−1, 0x13, 0x7F] same value mod p +``` + +Every constraint in the circuit is satisfied identically and the compression +output is bit-identical. The chip proves "the compression of these 64 cells" and +there are `p^3` cell-vectors per message word. Whether this becomes a Merkle +collision depends on the caller — for the §1.1 internal bus the receive tuple +carries the cells, so a byte-constrained counterparty pins them, and for the +§1.2 memory variant MEMW does. **The two gaps compose**: the gate models neither +the `AreBytes` nor the bus, so the obligation is discharged by nothing. + +**(3) The CV-only feed-forward trim — the deviation the design invites.** +`h[0..4]` land in round-0 `a` slots, and G uses `v[a]` only as an **add +operand**; it is never XORed. Their sole `ByteAlu` consumer is the *upper* +feed-forward half `out[i+8] = v[i+8] ⊕ h[i]`. The audit measures this: consumer +counts `[1,1,1,1,2,2,2,2]`, and for `h[0]` that single consumer is op #577 of 592 +— i.e. the feed-forward, not a round XOR. + +DESIGN §1.1 says: *"CV-only call sites read `out[0:8]`; the chip always produces +all 16 (the XOF root needs them)"*. For the internal 2-to-1 Merkle variant — the +primary target — an implementer dropping `out[8:16]` saves 32 committed cells and +32 sends (≈80 cell-equiv, 1.6%) and removes `h[0..4]`'s only range check. §7.4 +does not cover it, because it speaks of *add/shift outputs* and `h` is an input. +The gate reports the same UNSAT. + +### Fix + +Give the model a range-check object. Concretely: have `Circuit` allocate cells +that are *not* byte-bounded by default and add an explicit `are_bytes(word)` / +`byte_alu_xor()` that imposes the bound, so that "this word has no consumer" is +representable and shows up as SAT. That is a rewrite of the model into the field +(z3 `Int` mod p, as `audit_gate_transcription.py` §C does per-op) — but a cheaper +90% is available: keep BV and add the **structural** check this audit implements, +asserting that every `add`/`rotr` output word appears as a `xor` operand and that +`m` is explicitly range-checked. Twenty lines, and it is the check the design's +§7.4 actually asks for. + +--- + +## F2 — the width audit's shift control is run on a fragment + +`field_shift_bound` (`:380-395`) models **one halfword identity in isolation**: +`in_hw·2^r ≡ SLLC·2^16 + SLL (mod p)`, `SLLC ∈ [0,2^16)`, `SLL` unbounded, at one +concrete `in_hw = 0x9C3A` and one `r = 9`. It reports SAT and §9 renders that as + +> audit: **DROP `SLL` bound** (field neg ctrl) | **SAT** | without it the +> rotation is forgeable + +The chip does not contain that fragment. It contains **two** shift identities, +**two** recombine identities, and `Y` byte-range-checked by the downstream XOR. +Composing them (`audit_gate_transcription.py` §C2, symbolic over all 2³² inputs) +gives a different picture: + +| bounds kept | r=4 (rotr12) | r=9 (rotr7) | +|---|---|---| +| all four | PINNED | PINNED | +| `SLL_lo` only | **PINNED** | **PINNED** | +| `SLL_hi` only | **PINNED** | **PINNED** | +| `SLLC_lo` and/or `SLLC_hi`, no `SLL` | FORGEABLE | FORGEABLE | +| none | FORGEABLE | FORGEABLE | + +All 32 configurations were also checked non-vacuous (the honest witness +satisfies each). So: + +* **dropping one `SLL` bound is not exploitable at all** — the gate's control + claims it is; +* **dropping both** is exploitable at **exactly one input**. Enumerated + exhaustively, for both `r`: + +``` +X = 0xFFFFFFFF honest Y = 0xFFFFFFFF forged Y = 0x00000000 +SLL_lo = SLL_hi = p − 2^r (i.e. honest − 2^16 as a field element) +SLLC_lo = SLLC_hi = 2^r (i.e. honest + 1) +``` + +Every other `X` is UNSAT. The defence is still necessary — a prover can grind an +intermediate XOR output to `0xFFFFFFFF` cheaply, and there are 96 rotation slots +per compression — but "forgeable" at one point is not what the control shows, and +the control shows it for a chip that does not exist. + +**§7.3's mechanism is backwards.** It says *"dropping it makes the rotation +forgeable (a wrong `SLL` admits a **large field SLLC**)"*. `SLLC` is bounded to +`[0,2^16)` by its own `AreBytes` and stays small in the forgery (`2^r`); it is +`SLL` that goes large (`p − 2^r`). The witness above is the counterexample to the +prose, not to the conclusion. + +**Cost consequence, flagged not pursued:** §4.2 spends 4 `AreBytes` sends per +rotation. One suffices in this composed model. At 96 rotations that is 288 of +the design's ~1,250 sends per compression → ≈432 aux cells ≈ **8.6% of the 5,030 +cell-equiv budget**. Before acting on that, note it depends on `Y` being +byte-checked by its consumer — i.e. on F1's premise — and on `2^{-16} mod p` +being large, which is exactly the kind of implicit structural fact this audit +exists to distrust. It wants its own gate row, not a code change. + +By contrast `field_add_carry` (`:398-413`) **is** faithful: dropping the carry +booleanity is forgeable even with `s` byte-range-checked (verified composed), and +its UNSAT direction holds for all `(a,b,m)`, not just the one concrete triple it +tests. Both width-audit positives were re-derived symbolically and hold +universally. + +--- + +## F3 — "covers every G, hence every round" is the same argument as the free range check + +§9's MAIN 0 row carries the whole default run: + +> **covers every G, hence every round** (a round is a fixed composition of 8 +> G-calls). + +MAIN 0 proves: *for all byte-valued `v[a],v[b],v[c],v[d],mx,my`, the G's four +outputs equal `bref_g`*. Composing it needs each G's **inputs** to be +byte-valued, which holds because they are the previous G's outputs, which are +byte-valued because of the downstream-XOR range check. **The chaining argument +and the free-range-check argument are one argument.** The gate models neither: +the chaining is prose in §9, and the range check is the `BitVec(…,8)` +declaration. + +What *is* checkable, and what this audit checks mechanically because the gate +does not: + +* `build_round` calls `build_g` on the 8 quadruples of `G_CALLS`, in order, all + 7 rounds — compared against the quadruples recovered by instrumenting the + **oracle's** `round_fn`, not against the gate's own constant. +* `build_compress` feeds every G the original message column under `permute^r` + — all 7 rounds × 8 calls = **56 index pairs**, compared against the oracle's + permutation composition. Both tampers (a swapped `MSG_PERMUTATION` entry, a + swapped `mx/my` in `G_CALLS[5]`) are detected. +* Every G quadruple has four distinct state indices, the 8 calls touch each of + the 16 slots exactly twice and consume each message index exactly once — so + MAIN 0's `a,b,c,d = 0,1,2,3` instance really is general. + +README finding 2 ("in Rust, 48 G instances are emitted separately and a wrong +column index in instance #37 is not covered") stands and is out of scope: there +is no Rust. + +--- + +## F4 — what the model does not carry at all + +Not errors; scope. Listed because DESIGN cites the gate for some of them. + +| DESIGN claim | modelled? | consequence | +|---|---|---| +| §4.5 / §7.1 "every eval constraint is μ-gated; padding rows all-zero" | **no** — no μ variable exists; the docstring says "here mu=1 (a real row), so mu drops out" | exact for a live row. The gate says nothing about padding rows, so §7.1 is unbacked. Note the *ungated* system is strictly **stronger** as a system over all rows — it is only the single-live-row scope that makes this safe. | +| §4.8 degree ledger; the O1 (a)-vs-(c) decision | **no** — the model has no degree notion | `check_g` would be equally UNSAT for the rejected ternary-carry option (a). (a) is rejected for degree, not soundness, so this is harmless — but §4.8 is not gate-backed. | +| §1.1 `Blake3` bus, `Multiplicity::Column(MU)`, `TIMESTAMP_0/1` | **no** — confirmed absent: no `Multiplicity`, `TIMESTAMP`, `receive`/`send` anywhere in the file | README finding 1 (the missing input↔output timestamp binding) is invisible to the gate. Confirmed, not re-derived. | +| §4.1 "operands may be linear combos (sum ≤ 255)" | unexercised | `rotr16`/`rotr8` are pure index relabels, so every modelled operand is a single cell. Consistent with the design's actual use. | +| `block_len ∈ [0,64]`, `flags ∈ [0,128)` | modelled as free 32-bit words | model **weaker** — safe, and DESIGN specifies no such constraint either. | + +The BITWISE contracts the gate *assumes* were cross-checked and are real: +`prover/src/tables/bitwise.rs:351-364` enumerates `x,y ∈ [0,256)` and sets +`cols::XOR = x ^ y`; the `ByteAlu` receiver is at `:903-920` and the `AreBytes` +receiver at `:781-796`, both over that domain. DESIGN's cites (`:903`, `:783`) +are accurate. + +--- + +## The assertion tables + +Verdicts: **match** = model = object; **stronger** = model asserts more; +**weaker** = model omits; **not modelled** = outside the model entirely. +`file:line` refers to `blake3-chip/z3_blake_verify.py` unless noted. + +### (a) `blake3_ref.py` → `bref_*` + +Checked mechanically, not by eye: the G schedule and rotation amounts are +**recovered from the oracle by instrumentation** and compared, and every function +is differentially tested. + +| element | oracle | gate | verdict | +|---|---|---|---| +| `IV`, 8 words | `blake3_ref.py:29-32` | `:50-51` | match, element-wise | +| `MSG_PERMUTATION`, 16 indices | `:37` | `:52` | match, element-wise; and is a permutation of 0..15 | +| `MASK32` | `:54` | `:53` | match | +| G body: add order, XOR order, rotation amounts **16,12,8,7 in that order** | `:96-103` | `bref_g` `:72-80` | match — amounts recovered by patching `rotr`/`RotateRight` on both sides | +| G argument order (`v[a]+v[b]+mx` first, `my` second half) | `:96-100` | `:73,77` | match (differential, 300 random + 18 edge inputs) | +| `G_CALLS` 8 quadruples + message indices, **including the 4 diagonals** | `round_fn` `:113-121` | `:58-67` | **match, recovered from `round_fn` by instrumenting `g`** | +| `bref_round` iterates `G_CALLS` in order | `:113-121` | `:83-85` | match | +| `bref_permute` | `permute` `:126` | `:88-89` | match (index-identical) | +| initial `v`: `h[0..8]`, `IV[0..4]`, `t_lo`→v[12], `t_hi`→v[13], `block_len`→v[14], `flags`→v[15] | `compress` `:167-172` | `bref_compress` `:101-104` | match — probed slot by slot at rounds=0, and across 7 counters incl. `2^32−1`, `2^32` | +| counter split `t_lo = t mod 2^32`, `t_hi = t >> 32` | `:164-165` | `:509` (caller) | match | +| permutation applied `r < rounds−1`, i.e. **rounds−1 times** | `:181-182` | `:106-109` | match — counted by instrumentation for rounds 0..8 on both sides: `0,0,1,2,3,4,5,6,7` | +| feed-forward `out[i]=v[i]^v[i+8]`, `out[i+8]=v[i+8]^h[i]` with `h` the **original** CV | `:186-188` | `:110-114` | match | +| rounds parameterisation (6 vs 7 is the loop bound only) | `:176-182` | `:106-109` | match — differential over rounds {0,1,2,5,6,7,8} × 25 vectors | + +No discrepancy. Tampers on `IV`, `G_CALLS` and `MSG_PERMUTATION` are all +detected by the differentials. + +### (b) `DESIGN.md` → `build_g` / `build_round` / `build_compress` + +| DESIGN element | design | model | verdict | +|---|---|---|---| +| §4.1 XOR = 4 per-byte `ByteAlu[XOR]` sends, output pinned + operands range-checked | §4.1 | `xor` `:161-166`, 4 equalities | match (contract verified against `bitwise.rs:351-364`) | +| §4.2 `rotr16` = byte relabel `[b2,b3,b0,b1]`, free | §4.2/§7.6 | `:168-170` | **match — an actual index permutation of the source XOR's byte objects**, not a BV rotate; commits 0 columns, emits 0 constraints; and value-equal to `RotateRight(...,16)` over all 2³² | +| §4.2 `rotr8` = `[b1,b2,b3,b0]`, free | §4.2/§7.6 | `:172-174` | match, same evidence | +| §4.2 `rotr12 = rotl16∘rotl4` (r=4), `rotr7 = rotl16∘rotl9` (r=9) | §4.2 | `:207` `{12:4, 7:9}` | match | +| §4.2 two shift identities `hw·2^r = SLLC·2^16 + SLL` | §4.2 | `:222-223` | match | +| §4.2 recombine `Ylo = SLL_hi + SLLC_lo`, `Yhi = SLL_lo + SLLC_hi` | §4.2 | `:226-227` | match | +| §4.2 `SLL_*`, `SLLC_*` are 16-bit (2 bytes each) | §4.2 | `fresh_word()[:2]` `:213-216` | match | +| §4.2 4 `AreBytes` sends/rotation | §4.2 | **not modelled** | **stronger** (F1); and 3 of the 4 are not load-bearing (F2) | +| §4.3 2-op add: `a+b = s + 2^32·carry`, carry boolean | §4.3 | `add2` `:176-184` | match — the design's *derived* carry and the model's *committed* boolean carry proved equivalent over F_p | +| §4.4 3-op add: `a+b+m = s + 2^32·(c1+c2)`, `c1,c2` boolean | §4.4 | `add3` `:186-197` | match | +| §4.3/§4.4/§5 `s` byte-range-checked free by the next XOR | §4.3/§4.4/§5/§7.4 | `s = self.fresh_word()` (declared bytes) | **stronger** (F1) | +| §4.6 feed-forward, 16 XORs, `out[i+8] = v[i+8] ⊕ h[i]` | §4.6 | `:274-277` | match | +| §4.7 `m` needs explicit `AreBytes`; `h`,`t`,`bl`,`fl` are free | §4.7 | **not modelled** | **stronger** (F1). The *dataflow* half of the claim is verified here mechanically: `h`,`t_lo`,`t_hi`,`bl`,`fl` each feed an XOR, `m` does not | +| §5 every add/shift output feeds a downstream XOR | §5/§7.4 | **not modelled** | **stronger** (F1) — premise verified true here for ROUNDS ∈ {1,2,6,7}: 288 add/shift outputs, 0 unchecked | +| §3 per-G budget 56 byte-cells + 6 carry bits | §3 | counted from the model | match (56 / 6) | +| §2 per-G op mix: 2 add3, 2 add2, 4 xor, 2 shift-rotations, 1 rotr16, 1 rotr8 | §2/§5 | counted from the model | match | +| §7.7 `permute^r` wired from the original `M` columns | §7.7 | `:266-272` | match — 56 index pairs vs the oracle's composition | +| §7.8 IV inlined as constants at `v[8..12]` | §1.1/§7.8 | `const_word` `:258-261` | match | +| §7.9 all field expressions `< 2^35 ≪ p` | §7.9 | `WIDE = 48` | match — the ℤ identity and the mod-p identity proved equivalent under the byte bounds | +| §4.5 μ-gating, all-zero padding | §4.5/§7.1 | **not modelled** | not modelled (F4) | +| §4.8 degree ≤ 3 | §4.8 | **not modelled** | not modelled (F4) | +| §1.1 `Blake3` bus, μ multiplicity, timestamps | §1.1/§3 | **not modelled** | not modelled (F4; README finding 1) | + +### Gate hygiene + +| check | result | +|---|---| +| the G circuit's constraints are satisfiable on their own — MAIN 0's UNSAT is not vacuous | pass | +| the 6-round circuit's constraints are satisfiable on their own | pass | +| constraints emitted per op: xor 4, add2 2, add3 3, rotr12 4, rotr16 0 | pass | +| all 10 canonical 6-round fixtures reproduce from the live oracle (the positive controls are not anchored to a stale file) | pass | +| …and none of them equals the 7-round compression of the same input | pass | +| `gen_7round_vector` returns the oracle's own 7-round output | pass | +| the assumed `ByteAlu[XOR]` / `AreBytes` contracts match `prover/src/tables/bitwise.rs` | pass | +| `check_g()` UNSAT, `check_compress(0)` UNSAT, `check_g(swap_g_operand)` SAT (`--slow`) | pass | + +--- + +## Documentation defects (F5) + +Not soundness; a reader following the citations is misled. + +* **§7.3's mechanism is backwards.** "a wrong `SLL` admits a large field `SLLC`" + — the forgery keeps `SLLC` small (`2^r`, inside its own bound) and makes `SLL` + large (`p − 2^r`). Witness above. +* **§4.8's recombine row over-states its degree.** `μ·(Ylo − SLL_hi − SLLC_lo)` + is linear in committed columns → body degree 1, ×μ = 2. The table says 2 → 3. + Safe-side wrong; the "no constraint exceeds 3" verdict is unaffected. +* **§3 and §4.3 disagree on whether `add2` commits a carry column.** §3's per-G + table counts 1 carry bit per `add2` (6 per G); §4.3 makes it a *derived linear + expression* `(a+b−s)·INV_SHIFT_32` with no column. Semantically equivalent + (proved), but 96 cells per compression hang on the reading, in a design whose + headline is a cell count. +* The gate's docstring "Every committed column … is a FREE bitvector" should say + "a free **byte**" — the distinction is F1. + +--- + +## Could not determine + +Stated so the boundary is explicit rather than implied. + +1. **Anything about a Rust chip.** There is none. README finding 2 (48 G + instances emitted separately; a wrong column index in instance #37) is + unauditable until it exists, and F1's forgeries are all statements about what + a future implementation must not do. +2. **The bus layer.** Not modelled by the gate, not audited here. F1's message + and `h` constructions become live or benign depending on it; README finding 1 + (the missing input↔output timestamp binding) sits in the same place. + Confirmed absent from the gate, per the brief — not re-derived. +3. **The `--full` monolithic UNSATs** (`check_round`, `check_compress(2/6/7)`) + were **not run** — 30-40 min timeouts each. I audited the model they run on, + not their verdicts. Note that they inherit F1 in full: a monolithic 6-round + UNSAT is still an UNSAT about a model in which every cell is a declared byte. +4. **The `HWSL` inline soundness proof** the design defers to + (`../keccak-verify/hwsl_inline_test.py` Part 2) — that directory is not in + this artifact. §C's composed field model re-derives the shift-identity result + independently, so the conclusion does not rest on the missing file, but the + cited proof was not read. +5. **Completeness.** Every result here is about soundness (can a wrong witness + pass). Whether an honest trace generator can *produce* the witnesses — the + `AreBytes` send layout, the carry values — is unchecked; a mismatch there is + an unprovable honest witness, not a forgery. +6. **Whether the recovered artifact is byte-identical to the 2026-07-23 + original** (README's own open item). Unchanged by this audit. + +--- + +## Regression suite + +`audit_gate_transcription.py`, 73 checks (76 with `--slow`), all passing, every +one paired with a tamper that must break it: + +``` +A reference transcription (a): constants element-wise; G_CALLS and rotation + amounts RECOVERED from the oracle by instrumentation; differential g / + round / permute / compress over rounds {0,1,2,5,6,7,8}; counter split + across 2^32; permute-application count 0,0,1,2,3,4,5,6,7; v-layout probed + slot by slot. Tampers: IV, G_CALLS, MSG_PERMUTATION — all detected. +B circuit transcription (b): rotr16/rotr8 are index relabels by object + identity, commit no columns, value-equal to RotateRight; per-G cell and op + census; SSA range-check provenance over one G and over ROUNDS 1/2/6/7; + h[0..4]'s single feed-forward consumer; message indexing under permute^r, + 56 pairs; what the model does not represent (no range object, no mu, no + bus). Tampers: a wrong relabel, a G whose add output loses its XOR + consumer, a swapped MSG_PERMUTATION entry, a swapped G_CALLS message pair + — all detected. +C the dangerous direction, in the field: add2/add3 pinned with the range + check and FORGEABLE without it (concrete witness a=b=0x80000000 -> s=2^32); + the rotation output needs no range check of its own; the 32-configuration + bound lattice with non-vacuity; the composed forgery at X=0xFFFFFFFF, + enumerated exhaustively; both width-audit positives re-derived symbolically + for all inputs; the message-cell collision. +D hygiene: non-vacuity of MAIN 0 and the 6-round model; per-op constraint + counts; derived-vs-committed carry equivalence; canonical fixtures + reproduce from the live oracle; Z-vs-F_p equivalence of the WIDE=48 model; + the BITWISE contracts checked against prover/src/tables/bitwise.rs; + (--slow) the gate's own BV verdicts. +``` diff --git a/thoughts/blake3/TRANSCRIPTION-AUDIT.md b/thoughts/blake3/TRANSCRIPTION-AUDIT.md new file mode 100644 index 000000000..8e4e00cb0 --- /dev/null +++ b/thoughts/blake3/TRANSCRIPTION-AUDIT.md @@ -0,0 +1,192 @@ +# Transcription audit — does the BLAKE3 gate assert what the design and oracle say? + +Auditor: independent pass, 2026-07-29, branch `spike/blake3-recovered`. +Objects audited: + +- **oracle**: `blake3-oracle/blake3_ref.py` + `test_oracle.py` (does it define the right function?) +- **gate**: `blake3-chip/z3_blake_verify.py` against `blake3-chip/DESIGN.md` and the oracle + (is the constraint transcription faithful? can the model be stronger than the chip?) +- **the uncommitted fixes** on `DESIGN.md` / `test_oracle.py` (the 3 design findings + + harness defects #1/#3) — verified, see §5. + +Method mirrors `../ec-recover-opt/gate/TRANSCRIPTION-AUDIT.md`: only one direction is +dangerous. A model **weaker** than the chip yields spurious SAT (false alarm); a model +**stronger** than the chip yields UNSAT on a forgeable chip (false assurance), and no +positive anchor can see that, because an honest witness satisfies a correct model and an +over-strong one equally well. Here there is no Rust chip yet — the gate is the only +executable statement of the design — so the audit is gate ↔ design + oracle, and every +place the gate *cannot see* is a place the future Rust must get right by construction. + +Reproduce: everything below ran with `blake3/venv` (z3 5.0.0, blake3 PyPI 1.x), +`ground-truth` (official `blake3` crate v1.8.5, pure-Rust), and the vendored +`others/Plonky3/blake3-air`. Mutant/tamper scripts were scratch files, not committed. + +--- + +## Verdict + +**No over-strong or mis-transcribed premise found.** Every equation in the gate matches +the design it encodes (§2 table), the gate's reference is behaviourally identical to the +externally-anchored oracle (§3), the gate is *sensitive* to every wiring-bug class we +could construct — including classes with no shipped negative control (§4, 7/7 mutants +fire) — and the width analysis holds with slack (expressions ≤ ~2^41 vs the 2^48 model +width). The two re-runs reproduce the recorded board: default run **OVERALL: PASS**; +`--full` monolithic UNSATs: **SEE §6**. + +The honest map of what a green board does NOT cover (§2, "no automated check" rows) +is where the remaining risk lives: μ-gating/padding, input range checks, the degree-3 +ledger, the bus layer, and the precomputed-table contracts. All are documented in +DESIGN §7; the uncommitted fixes added items 10–11. None is new. + +## §1 — Oracle re-validation (does the oracle define the right function?) + +Re-ran and independently re-derived, all green: + +| check | result | +|---|---| +| harness `test_oracle.py`, anchor 1 (official-parameter vectors) | PASS 35/35 × 3 modes | +| anchor 2 (official `blake3` PyPI pkg) — **live this time** (was SKIP) | PASS 92/92 | +| anchor 3 (Plonky3 `blake3-air` port, direct compression) | PASS 20 000/20 000 | +| banner honesty (defect #1 fix) | reads VALIDATED only because all three ran (see §5) | +| known-answer: `blake3("")`, `blake3("abc")` | exact match to published digests | +| differential vs PyPI: 140 lengths × {default, keyed, derive} + 48 XOF-length checks | 468/468 | +| counter split `t_lo/t_hi` vs official crate, XOF `set_position` path, t ∈ {0,1,2, 2^32−2, 2^32−1, **2^32**, **2^32+1**, 2^40, 2^47} | **9/9** (scratch `counter_probe.rs` + `blake3_ref.compress`) | +| swapped-halves negative control | breaks 7/9; the 2 invariants are t=0 and t=0x1_0000_0001 (t_lo==t_hi), both correctly invariant | +| message schedule count+direction: `permute^r` from identity vs the crate's precomputed `MSG_SCHEDULE` | all 7 rows exact | + +The historical counts ("35/35×3", "92/92") now reproduce as recorded. ORACLE.md O5 +(counter width) remains closed — re-confirmed against the crate at t ≥ 2^32. + +## §2 — Per-premise transcription table (gate ↔ DESIGN ↔ oracle) + +| gate premise | source | verified | how | +|---|---|---|---| +| `IV`, `MSG_PERMUTATION` constants | DESIGN §1, oracle §2.1, Plonky3 `constants.rs` | ✅ exact | 3-way diff | +| `G_CALLS` (8 index tuples + msg order) | oracle `round_fn` | ✅ exact | diff | +| `bref_*` reference independent of circuit wiring | DESIGN §8 | ✅ | 200 concrete trials vs `blake3_ref.compress` (rounds 6+7), 0 mismatch; leading-permute mutant differs ⇒ `r < rounds−1` guard direction correct | +| init layout `v = h ‖ IV[0..4] ‖ t_lo,t_hi,bl,fl` | oracle §2.4, DESIGN §7.8 | ✅ | MAIN 1 UNSAT + `wrong_iv` control | +| feed-forward `out[i]=v[i]⊕v[i+8]`, `out[i+8]=v[i+8]⊕h[i]` | oracle §2.4, DESIGN §4.6 | ✅ | MAIN 1 UNSAT + `drop_ff_xor` control | +| schedule = `permute^r` of the ORIGINAL `M` | DESIGN §7.7 | ✅ | `wrong_msg_index` control + `permute_inverse` mutant + positive controls | +| `add2`: `a+b = s + 2^32·c`, c boolean | DESIGN §4.3 | ✅ | equation exact; field-level necessity of booleanity confirmed (this audit, §4) | +| `add3`: `a+b+m = s + 2^32·(c1+c2)`, c1,c2 boolean | DESIGN §4.4 (O1 option c) | ✅ | equation exact; width audit drop→SAT | +| `rotr16=[b2,b3,b0,b1]`, `rotr8=[b1,b2,b3,b0]` free relabels | DESIGN §4.2/§7.6 | ✅ | relabel mutants flip check to SAT (§4) | +| `rotr12/rotr7` shift identity `hw·2^r = SLLC·2^16 + SLL`, r=4/9 | DESIGN §4.2 | ✅ | equation exact; `rot_wrong_amount` control | +| recombine `Ylo=SLL_hi+SLLC_lo`, `Yhi=SLL_lo+SLLC_hi` | DESIGN §4.2 | ✅ | recombine mutants flip to SAT (§4) | +| ByteAlu[XOR] / AreBytes table contracts | `prover/src/tables/bitwise.rs` | ⚠ assume-guarantee | documented; same assumption keccak gate makes; **no automated check here** | +| μ-gating / all-zero padding (μ=1 modelled) | DESIGN §4.5, §7.1 | ⚠ gate cannot see | no bus/multiplicity layer; on the implementer | +| input range checks (h,t,bl,fl free via XOR; **m needs explicit AreBytes**) | DESIGN §4.7, §7.5 | ⚠ gate cannot see | gate inputs are bytes by construction; a dropped `AreBytes(m)` in Rust is invisible here | +| degree ≤ 3 ledger | DESIGN §4.8 | ⚠ no automated check | manual ledger; gate models equations, not degrees | +| `Blake3` bus TIMESTAMP binding (findings fix) | DESIGN §1.1/§7.10 | ⚠ gate cannot see | no bus layer; verified by construction, see §5 | +| 48 G instances wired as MAIN 0 models | DESIGN §7.11 | ✅ concrete | positive controls run all 48; `--full` monolithic UNSATs (§6); per-instance index mutant fires (§4) | +| WIDE=48 model cannot wrap | gate internals | ✅ | worst expression ≈ 2^41 (add3 with 8-bit carries) ≪ 2^48 | + +## §3 — Reference (`bref_*`) independence + +The gate's soundness rests on `bref_*` being an independent statement of BLAKE3. It is +structurally independent (32-bit BV `RotateRight`/`+`/`^` vs the byte-level circuit) and +behaviourally identical to the oracle: 200 random concrete inputs, rounds ∈ {6,7}, +0 mismatches. The permute guard `r < rounds−1` matches the oracle's (a leading-extra-permute +variant provably differs). The one structural mirror both share with the oracle — the +constants and `G_CALLS` table — is pinned by the *external* anchors (crate, PyPI, Plonky3), +so a common-mode bug there would have to be a bug in BLAKE3 itself. + +## §4 — Gate sensitivity: shipped controls + mutation sweep + +Shipped controls all reproduced (default run): 5/5 structural SAT, width audit 4/4, +positive controls 4/4 SAT. + +Mutation sweep (scratch, not committed) — bug classes with **no shipped negative control**; +each was injected into a copy of the circuit builders and must flip its check to SAT: + +| mutant | class | result | +|---|---|---| +| `rotr16_bad_relabel` | free-rotation byte order (DESIGN §7.6) | **sat — detected** | +| `rotr8_bad_relabel` | free-rotation byte order | **sat — detected** | +| `rotr12_bad_recombine` | carry paired to wrong halfword | **sat — detected** | +| `swap_mx_my` | message operand order in G | **sat — detected** | +| `permute_inverse` | schedule direction | **sat — detected** | +| `bad_diag_index` | one wrong column in G instance #7 (per-instance wiring, §7.11) | **sat — detected** | +| `rounds_off_by_one` | round-loop bound | **sat — detected** | + +Field-level addition: the shipped width audit demonstrates bound-necessity only for the +**3-op** add. This audit verified the same for the **2-op** add: booleanity present → +UNSAT (pinned), dropped → SAT (forgeable mod p). Same class, now demonstrated for both. + +## §5 — Verdict on the uncommitted fixes + +- **Finding 1 (bus input↔output binding) — FIX REAL.** `DESIGN.md` §1.1/§3/§7.10 now + mandate `TIMESTAMP_0/1` in both `Blake3` receive and send. The cited precedent checks + out: `prover/src/tables/keccak.rs:264-319` sends `(ts, 0, input_state)` and receives + `(ts, 24, output_state)` on the internal `Keccak` bus with `TIMESTAMP_0/1` in *both* + tuples (`BusValue::Packed` at `cols::TIMESTAMP_0/1`). The swap-attack reasoning is + sound: with no common key, rows A/B exchanging output tuples keeps every tuple + appearing once per side, so LogUp balances while both callers read wrong results. + Correctly documented as gate-invisible (no bus layer). +- **Finding 2 ("covers every G" is a model argument) — FIX REAL.** §7.11 records it; + the positive controls do run the full 48-instance pipeline concretely, and this audit's + per-instance index mutant + the `--full` monolithic UNSATs (§6) back it. +- **Finding 3 (carry encoding ambiguity `(1,0)`/`(0,1)`) — correctly classified + harmless.** The sum identity constrains only `c1+c2`; `s` is pinned regardless. +- **Harness defect #1 (banner overstatement) — FIX REAL.** The banner now reads from + the status dict; exercised live (below). +- **Harness defect #3 (missing-fixture cascade) — FIX REAL.** With + `official_test_vectors.json` renamed away: anchor 1 SKIPs alone, anchors 2/3 PASS, + the canonical-vector emitter still runs (it is now unconditional), banner reads + "PARTIALLY VALIDATED … NOT anchored on: official-parameter vectors". Fixture restored + afterwards; regenerated `canonical_6round_vectors.json` is byte-identical. + +## §6 — Gate re-runs + +- default (`z3_blake_verify.py`), z3 5.0.0: **OVERALL: PASS** (board identical to §9 of + DESIGN.md). +- `--full` (monolithic symbolic round / rounds=2 / 6-round / 7-round UNSATs): + **PENDING — fill in when the background run completes.** + +## §6b — Reconciliation with the second, independent audit (`audit_gate_transcription.py`) + +A separately-authored executable audit (74/74 checks pass, run this session) agrees with +every verdict above and sharpens three points this audit stated more coarsely: + +1. **Rotation bound necessity, refined.** The load-bearing bound set is *at least one of* + `{SLL_lo, SLL_hi}` — every configuration with neither is forgeable, every one with + either is pinned; the `SLLC` bounds are not load-bearing. DESIGN §4.2's "the tight + SLL bound" should read "a tight bound on at least one SLL halfword". The composed + (whole-rotation) forgery with both SLL bounds dropped exists for exactly **one** + input, `X=0xFFFFFFFF` (forged `Y=0`), not for arbitrary inputs. +2. **Doc note (safe direction):** DESIGN §4.8's degree-ledger row for the recombine + identity overstates (claims body 2 → 3 after ×μ; the body is linear, so 1 → 2). + The "no constraint exceeds 3" verdict is unaffected. +3. **Doc/cost inconsistency:** DESIGN §3's per-G table commits 1 carry *column* per add2, + while §4.3 makes that carry a *derived* linear expression (`(a+b−s)·INV_SHIFT_32`). + The two are equivalent over F_p (proven by that suite) but differ by 96 cells per + compression in the §6 cost table. The gate models the committed form. + +It also independently confirms this report's two "gate cannot see" rows with explicit +forgeries: the missing `AreBytes(m)` (§2, DESIGN §4.7) and the declared-not-derived +input range checks. + +## §7 — Still open (pre-existing, report-only per audit scope) + +1. Harness defect #2: `test_internal_consistency`'s comment promises a feed-forward + recomputation that is not implemented (`test_oracle.py:227`). Comment lies; check is + shallow (length + CV prefix only). +2. Harness defect #4: `test_6round_derivation`'s first assertion is a tautology + (`compress_6round` *is* `compress(rounds=6)`). The differs-from-7r half is the real + content. +3. Harness defect #5 (footgun for the Rust phase): `compress(...)` defaults `rounds=7`. + Trace generators must call `compress_6round` / pass `rounds=` explicitly. +4. ORACLE.md §5 prose "¼–⅓ of a keccak permutation" is inconsistent with its own + ~5–6k figure (≈1/6 of 24×1480); superseded by DESIGN §6's derived ≈1/15. Doc-only. +5. `ground-truth/Cargo.toml` could not build inside this repo ("believes it's in a + workspace"). Fixed with an empty `[workspace]` table — **this audit touched that one + committed file**; without it the documented regeneration flow fails out of the box. +6. Suggestion (not a defect): fold the §4 mutant sweep and the add2 field check into the + shipped gate as regression controls, so future edits to the gate are held to the same + sensitivity. + +## §8 — Scratch artifacts left in the tree (untracked; commit or delete, user's call) + +- `thoughts/blake3/venv/` (z3 5.0.0 + official `blake3` PyPI pkg) +- `thoughts/blake3/ground-truth/src/bin/counter_probe.rs` (the t≥2^32 counter probe) +- `thoughts/blake3/ground-truth/target/`, `thoughts/blake3/blake3-oracle/__pycache__/` +- mutant sweep + add2 field check: `/tmp/blake_mutants.py` and heredocs (not in tree) diff --git a/thoughts/blake3/audit_gate_transcription.py b/thoughts/blake3/audit_gate_transcription.py new file mode 100644 index 000000000..6cd9b1000 --- /dev/null +++ b/thoughts/blake3/audit_gate_transcription.py @@ -0,0 +1,1198 @@ +""" +Transcription audit of `blake3-chip/z3_blake_verify.py` — an EXECUTABLE +regression suite for GATE-TRANSCRIPTION-AUDIT.md. + +Two transcriptions are under test, and only one direction is dangerous. + + (a) blake3-oracle/blake3_ref.py -> the gate's `bref_*` BV reference. + If these diverge, every UNSAT the gate reports proves the chip matches + the WRONG function. + + (b) blake3-chip/DESIGN.md -> the gate's `build_g/build_round/ + build_compress` circuit model. + A model WEAKER than the designed chip yields a spurious SAT — a false + alarm, safe. A model STRONGER yields UNSAT where the real object is + forgeable — false assurance, and no positive control can see it, + because an honest witness satisfies a correct model and an over-strong + model equally well. + +Every check below is paired with a TAMPER that must make it fail; a check +that does not bite is itself reported as a failure. Nothing outside this +file is modified: tampers are applied to in-memory copies and reverted. + +Run: python3 audit_gate_transcription.py (fast sections) + python3 audit_gate_transcription.py --slow (+ the BV UNSATs, ~5 min) +""" +import importlib.util +import itertools +import os +import random +import sys + +from z3 import ( + And, BitVec, BitVecVal, Concat, Int, IntVal, Or, RotateRight, Solver, + is_bv, sat, simplify, unsat, +) + +HERE = os.path.dirname(os.path.abspath(__file__)) +P = 2**64 - 2**32 + 1 # Goldilocks +MASK32 = 0xFFFFFFFF + + +def _load(name, relpath): + spec = importlib.util.spec_from_file_location(name, os.path.join(HERE, relpath)) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +ORA = _load("blake3_ref", "blake3-oracle/blake3_ref.py") +GATE = _load("z3_blake_verify", "blake3-chip/z3_blake_verify.py") + + +# --------------------------------------------------------------------------- +# result bookkeeping +# --------------------------------------------------------------------------- +RESULTS = [] + + +def record(section, name, ok, detail=""): + RESULTS.append((section, name, bool(ok), detail)) + flag = "PASS" if ok else "**FAIL**" + print(f" [{flag}] {name}" + (f" {detail}" if detail else "")) + return ok + + +def note(text): + """An observation that is reported but is not a pass/fail check.""" + print(f" [note] {text}") + + +def ora_G_CALLS(): + """The G-call schedule RECOVERED from the oracle (blake3_ref.round_fn), + with sentinel message words so the mx/my indices come back too.""" + calls = [] + orig = ORA.g + + def spy(state, a, b, c, d, mx, my): + calls.append((a, b, c, d, mx - 1000, my - 1000)) + ORA.g = spy + try: + ORA.round_fn([0] * 16, [1000 + i for i in range(16)]) + finally: + ORA.g = orig + return calls + + +def bites(section, name, tamper_fn): + """A check must FAIL under its tamper, or the check is decorative.""" + try: + detected = tamper_fn() + except Exception as exc: # a crash is also detection + detected = True + record(section, f"tamper bites: {name}", True, f"(raised {type(exc).__name__})") + return True + return record(section, f"tamper bites: {name}", detected, + "" if detected else "TAMPER NOT DETECTED — the check is vacuous") + + +# =========================================================================== +# SECTION A — transcription (a): blake3_ref.py -> bref_* +# =========================================================================== +def section_A(slow): + print("\n" + "=" * 74) + print("A REFERENCE TRANSCRIPTION — blake3_ref.py -> bref_* (the BV oracle)") + print("=" * 74) + + # -- A1 constant tables, element by element ---------------------------- + record("A", "IV identical (8 words, element-wise)", + list(GATE.IV) == list(ORA.IV), f"{[hex(x) for x in GATE.IV[:2]]}...") + record("A", "MSG_PERMUTATION identical (16 indices, element-wise)", + list(GATE.MSG_PERMUTATION) == list(ORA.MSG_PERMUTATION), + str(GATE.MSG_PERMUTATION)) + record("A", "MSG_PERMUTATION is a permutation of 0..15", + sorted(GATE.MSG_PERMUTATION) == list(range(16))) + record("A", "MASK32 identical", GATE.MASK32 == ORA.MASK32) + + def tamper_iv(): + old = GATE.IV[3] + GATE.IV[3] ^= 1 + bad = list(GATE.IV) != list(ORA.IV) + GATE.IV[3] = old + return bad + bites("A", "IV comparison", tamper_iv) + + # -- A2 the G-call schedule, RECOVERED from the oracle ------------------ + ora_calls = ora_G_CALLS() + record("A", "G_CALLS == the oracle's round_fn call sequence (recovered)", + [tuple(x) for x in GATE.G_CALLS] == ora_calls, + f"{len(ora_calls)} calls") + record("A", "every G quadruple has 4 DISTINCT state indices " + "(so check_g's a,b,c,d=0,1,2,3 instance is general)", + all(len({a, b, c, d}) == 4 for a, b, c, d, _, _ in GATE.G_CALLS)) + record("A", "the 8 G-calls touch each of the 16 state slots exactly twice", + sorted(i for q in GATE.G_CALLS for i in q[:4]) == + sorted(list(range(16)) * 2)) + record("A", "the 8 G-calls consume message indices 0..15 exactly once", + sorted(i for q in GATE.G_CALLS for i in q[4:]) == list(range(16))) + + def tamper_gcalls(): + old = GATE.G_CALLS[4] + GATE.G_CALLS[4] = (0, 5, 10, 15, 9, 8) # mx/my swapped + bad = [tuple(x) for x in GATE.G_CALLS] != ora_calls + GATE.G_CALLS[4] = old + return bad + bites("A", "G_CALLS comparison", tamper_gcalls) + + # -- A3 rotation amounts and their ORDER, recovered from both sides ----- + def oracle_rot_amounts(): + seen = [] + orig = ORA.rotr + + def spy(x, n): + seen.append(n) + return orig(x, n) + ORA.rotr = spy + try: + ORA.g([0] * 4, 0, 1, 2, 3, 0, 0) + finally: + ORA.rotr = orig + return seen + + def bref_rot_amounts(): + seen = [] + orig = GATE.RotateRight + + def spy(x, n): + seen.append(n) + return orig(x, n) + GATE.RotateRight = spy + try: + GATE.bref_g([BitVec(f"a{i}", 32) for i in range(4)], 0, 1, 2, 3, + BitVec("mx", 32), BitVec("my", 32)) + finally: + GATE.RotateRight = orig + return seen + + ora_rots, bref_rots = oracle_rot_amounts(), bref_rot_amounts() + record("A", "bref_g rotation amounts and order == oracle g", + ora_rots == bref_rots == [16, 12, 8, 7], f"{bref_rots}") + + # -- A4 differential: bref_* vs the oracle on concrete values ----------- + rng = random.Random(0xB1A3E) + + def w32(v): + return BitVecVal(v & MASK32, 32) + + def as_int(bv): + return simplify(bv).as_long() + + def diff_g(n): + for _ in range(n): + st = [rng.randrange(1 << 32) for _ in range(4)] + mx, my = rng.randrange(1 << 32), rng.randrange(1 << 32) + ref = list(st) + ORA.g(ref, 0, 1, 2, 3, mx, my) + bv = [w32(x) for x in st] + GATE.bref_g(bv, 0, 1, 2, 3, w32(mx), w32(my)) + if [as_int(x) for x in bv] != ref: + return False, (st, mx, my) + return True, None + + ok, cex = diff_g(300) + record("A", "bref_g == oracle g (300 random + carry/rotate edge inputs)", ok, + "" if ok else f"counterexample {cex}") + + # edge cases: all-zero, all-ones, single bits (exercise every carry and + # every rotate boundary) + edges = [[0] * 4, [MASK32] * 4, [1, 0, 0, 0], [0, 0, 0, MASK32], + [0x80000000] * 4, [0x0000FFFF, 0xFFFF0000, 0xF0F0F0F0, 0x0F0F0F0F]] + ok_edge = True + for st in edges: + for msg in ([0, 0], [MASK32, MASK32], [0x80000000, 1]): + ref = list(st) + ORA.g(ref, 0, 1, 2, 3, msg[0], msg[1]) + bv = [w32(x) for x in st] + GATE.bref_g(bv, 0, 1, 2, 3, w32(msg[0]), w32(msg[1])) + ok_edge &= ([as_int(x) for x in bv] == ref) + record("A", "bref_g == oracle g (edge inputs: 0, 2^32-1, MSB, split words)", + ok_edge) + + def diff_round(n): + for _ in range(n): + st = [rng.randrange(1 << 32) for _ in range(16)] + m = [rng.randrange(1 << 32) for _ in range(16)] + ref = list(st) + ORA.round_fn(ref, m) + got = GATE.bref_round_only([w32(x) for x in st], [w32(x) for x in m]) + if [as_int(x) for x in got] != ref: + return False + return True + record("A", "bref_round_only == oracle round_fn (60 random states+messages)", + diff_round(60)) + + m0 = [rng.randrange(1 << 32) for _ in range(16)] + record("A", "bref_permute == oracle permute (and is index-identical)", + [as_int(x) for x in GATE.bref_permute([w32(x) for x in m0])] + == ORA.permute(m0)) + + def diff_compress(n, rounds_list): + for _ in range(n): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(65) + fl = rng.randrange(128) + for r in rounds_list: + ref = ORA.compress(h, m, t, bl, fl, rounds=r) + got = GATE.bref_compress([w32(x) for x in h], [w32(x) for x in m], + w32(t & MASK32), w32((t >> 32) & MASK32), + w32(bl), w32(fl), r) + if [as_int(x) for x in got] != ref: + return False, (h, m, t, bl, fl, r) + return True, None + + ok, cex = diff_compress(25, [0, 1, 2, 5, 6, 7, 8]) + record("A", "bref_compress == oracle compress (25 vectors x rounds " + "{0,1,2,5,6,7,8}) — pins the rounds parameterisation", ok, + "" if ok else f"counterexample {cex}") + + # counters straddling 2^32 — the split order t_lo=v[12], t_hi=v[13] + ok_ctr = True + for t in (0, 1, 2**32 - 1, 2**32, 2**32 + 1, 2**47 + 12345, 2**64 - 1): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + ref = ORA.compress(h, m, t, 64, 3, rounds=6) + got = GATE.bref_compress([w32(x) for x in h], [w32(x) for x in m], + w32(t & MASK32), w32((t >> 32) & MASK32), + w32(64), w32(3), 6) + ok_ctr &= ([as_int(x) for x in got] == ref) + record("A", "counter split order matches across 2^32 " + "(t_lo->v[12], t_hi->v[13]; 7 counters incl. 2^32-1, 2^32)", ok_ctr) + + def tamper_bref_perm(): + old = GATE.MSG_PERMUTATION[:] + GATE.MSG_PERMUTATION[0], GATE.MSG_PERMUTATION[1] = old[1], old[0] + bad = not diff_compress(3, [2, 6])[0] + GATE.MSG_PERMUTATION[:] = old + return bad + bites("A", "bref_compress differential (permutation)", tamper_bref_perm) + + def tamper_bref_iv(): + old = GATE.IV[0] + GATE.IV[0] ^= 1 + bad = not diff_compress(2, [1, 6])[0] + GATE.IV[0] = old + return bad + bites("A", "bref_compress differential (IV)", tamper_bref_iv) + + # -- A5 how many times the permutation is applied ---------------------- + def ora_permute_count(rounds): + n = [0] + orig = ORA.permute + + def spy(m): + n[0] += 1 + return orig(m) + ORA.permute = spy + try: + ORA.compress([0] * 8, [0] * 16, 0, 64, 0, rounds=rounds) + finally: + ORA.permute = orig + return n[0] + + def bref_permute_count(rounds): + n = [0] + orig = GATE.bref_permute + + def spy(m): + n[0] += 1 + return orig(m) + GATE.bref_permute = spy + try: + GATE.bref_compress([w32(0)] * 8, [w32(0)] * 16, w32(0), w32(0), + w32(0), w32(0), rounds) + finally: + GATE.bref_permute = orig + return n[0] + + counts = [(r, ora_permute_count(r), bref_permute_count(r)) for r in range(9)] + record("A", "permute applications per rounds r == max(r-1,0), oracle == bref " + "(the classic off-by-one)", + all(o == b == max(r - 1, 0) for r, o, b in counts), + " ".join(f"r{r}:{b}" for r, _, b in counts)) + + # -- A6 the initial-state layout, probed slot by slot ------------------- + # rounds=0 makes out[i]=v[i]^v[i+8] and out[i+8]=v[i+8]^h[i] read the + # initial state directly, so each slot is individually observable. + h = [0] * 8 + m = [0] * 16 + tlo, thi, bl, fl = 0xA1A2A3A4, 0xB1B2B3B4, 0xC1C2C3C4, 0xD1D2D3D4 + out0 = [as_int(x) for x in GATE.bref_compress( + [w32(x) for x in h], [w32(x) for x in m], w32(tlo), w32(thi), + w32(bl), w32(fl), 0)] + layout_ok = ( + out0[0] == GATE.IV[0] and out0[1] == GATE.IV[1] and + out0[2] == GATE.IV[2] and out0[3] == GATE.IV[3] and + out0[4] == tlo and out0[5] == thi and out0[6] == bl and out0[7] == fl and + out0[8] == GATE.IV[0] and out0[12] == tlo and out0[13] == thi) + record("A", "initial v layout: v[8..12]=IV, v[12]=t_lo, v[13]=t_hi, " + "v[14]=block_len, v[15]=flags (probed slot by slot)", layout_ok, + f"out[4..8]={[hex(x) for x in out0[4:8]]}") + + hh = [0x11111111 * (i + 1) for i in range(8)] + out1 = [as_int(x) for x in GATE.bref_compress( + [w32(x) for x in hh], [w32(x) for x in m], w32(0), w32(0), w32(0), + w32(0), 0)] + ff_ok = (all(out1[i] == (hh[i] ^ [GATE.IV[0], GATE.IV[1], GATE.IV[2], + GATE.IV[3], 0, 0, 0, 0][i]) + for i in range(8)) and + all(out1[i + 8] == ([GATE.IV[0], GATE.IV[1], GATE.IV[2], GATE.IV[3], + 0, 0, 0, 0][i] ^ hh[i]) for i in range(8))) + record("A", "feed-forward: out[i]=v[i]^v[i+8] and out[i+8]=v[i+8]^h[i] " + "with h the ORIGINAL input CV (not the mutated state)", ff_ok) + + +# =========================================================================== +# SECTION B — transcription (b): DESIGN.md -> build_g / build_round / +# build_compress. Structural, by instrumenting the model. +# =========================================================================== +class Traced(GATE.Circuit): + """Circuit subclass that records the SSA dataflow the model builds. + + Words are lists of z3 byte expressions; rotr16/rotr8 return the SAME byte + objects (they are relabels), so resolving an operand's bytes to their + producing word automatically follows a free rotation back to its source + XOR — which is exactly the provenance question DESIGN 4.3/4.4/5 raises. + """ + + def __init__(self, tag, bug=None): + super().__init__(tag, bug) + self.words = [] # wid -> {kind, cells, used} + self.owner = {} # byte-expr name -> wid + self.ops = [] # {kind, ins:[wid], outs:[wid], perm:[..]} + self._pending = [] + + # -- registration ------------------------------------------------------ + def fresh_word(self): + w = super().fresh_word() + wid = len(self.words) + self.words.append({"kind": "unassigned", "cells": w, "used": 4}) + for c in w: + self.owner[str(c)] = wid + self._pending.append(wid) + return w + + def const_word(self, val): + w = super().const_word(val) + wid = len(self.words) + self.words.append({"kind": "const", "cells": w, "used": 4}) + return w + + def _wids(self, word): + return sorted({self.owner[str(c)] for c in word if str(c) in self.owner}) + + def _op(self, kind, ins, out_kinds, perm=None): + outs = self._pending[:] + self._pending = [] + for wid, k in zip(outs, out_kinds): + self.words[wid]["kind"] = k + self.ops.append({"kind": kind, + "ins": [self._wids(w) for w in ins], + "in_words": ins, "outs": outs, "perm": perm}) + return outs + + # -- the operations under contract ------------------------------------ + def xor(self, A, B): + self._pending = [] + out = super().xor(A, B) + self._op("xor", [A, B], ["xor_out"]) + return out + + def add2(self, A, B, drop_bool=False): + self._pending = [] + out = super().add2(A, B, drop_bool) + self._op("add2", [A, B], ["add_out"]) + return out + + def add3(self, A, B, M, drop_bool=False): + self._pending = [] + out = super().add3(A, B, M, drop_bool) + self._op("add3", [A, B, M], ["add_out"]) + return out + + def rotr(self, A, n, wrong_amount=False): + self._pending = [] + out = super().rotr(A, n, wrong_amount) + # fresh_word order inside Circuit.rotr: sll_lo, sllc_lo, sll_hi, + # sllc_hi, Y (the first four are used two bytes wide) + outs = self._op("rotr", [A], ["sll", "sllc", "sll", "sllc", "rot_out"], + perm=n) + for wid in outs[:4]: + self.words[wid]["used"] = 2 + return out + + def rotr16(self, A): + out = super().rotr16(A) + self.ops.append({"kind": "relabel16", "ins": [self._wids(A)], + "in_words": [A], "outs": [], + "perm": [A.index(c) if c in A else None for c in out]}) + return out + + def rotr8(self, A): + out = super().rotr8(A) + self.ops.append({"kind": "relabel8", "ins": [self._wids(A)], + "in_words": [A], "outs": [], + "perm": [A.index(c) if c in A else None for c in out]}) + return out + + # -- provenance analysis ---------------------------------------------- + def xor_consumed(self): + """wids that appear as an operand of at least one ByteAlu[XOR].""" + s = set() + for op in self.ops: + if op["kind"] == "xor": + for group in op["ins"]: + s.update(group) + return s + + def unchecked(self): + """SSA words whose byte-range DESIGN 4.3/4.4/5 sources from a + downstream XOR, but which no XOR in this scope consumes.""" + xc = self.xor_consumed() + return [wid for wid, w in enumerate(self.words) + if w["kind"] in ("add_out", "rot_out") and wid not in xc] + + +def _build_one_g(cls=Traced, build=None): + cir = cls("aud") + v = [None] * 16 + va, vb, vc, vd = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + mx, my = cir.fresh_word(), cir.fresh_word() + for w in (va, vb, vc, vd, mx, my): + cir.words[cir.owner[str(w[0])]]["kind"] = "input" + v[0], v[1], v[2], v[3] = va, vb, vc, vd + (build or GATE.build_g)(cir, v, 0, 1, 2, 3, mx, my, None, False) + return cir, v, (va, vb, vc, vd, mx, my) + + +def _build_compress(rounds): + cir = Traced("audc") + h = [cir.fresh_word() for _ in range(8)] + m = [cir.fresh_word() for _ in range(16)] + tlo, thi, bl, fl = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + for w in h + m + [tlo, thi, bl, fl]: + cir.words[cir.owner[str(w[0])]]["kind"] = "input" + out = GATE.build_compress(cir, h, m, tlo, thi, bl, fl, rounds) + return cir, out, dict(h=h, m=m, tlo=tlo, thi=thi, bl=bl, fl=fl) + + +def section_B(slow): + print("\n" + "=" * 74) + print("B CIRCUIT TRANSCRIPTION — DESIGN.md -> build_g / build_round / " + "build_compress") + print("=" * 74) + + # -- B1 the free rotations are byte relabels, not BV rotates ----------- + cir = GATE.Circuit("rel") + A = cir.fresh_word() + n_before = cir.n + r16, r8 = cir.rotr16(A), cir.rotr8(A) + record("B", "rotr16 is the index relabel [b2,b3,b0,b1] on the SOURCE bytes " + "(object identity, DESIGN 4.2/7.6)", + [c is A[i] for c, i in zip(r16, (2, 3, 0, 1))] == [True] * 4) + record("B", "rotr8 is the index relabel [b1,b2,b3,b0] on the SOURCE bytes", + [c is A[i] for c, i in zip(r8, (1, 2, 3, 0))] == [True] * 4) + record("B", "the free rotations commit NO new columns and emit NO " + "constraints (DESIGN 3: 'produce no columns')", + cir.n == n_before and cir.C == []) + + s = Solver() + s.add(Or(cir.word32(r16) != RotateRight(cir.word32(A), 16), + cir.word32(r8) != RotateRight(cir.word32(A), 8))) + record("B", "and the relabels are VALUE-equal to RotateRight 16 / 8 " + "(z3, all 2^32 inputs)", s.check() == unsat) + + def tamper_relabel(): + w = GATE.Circuit("t") + B = w.fresh_word() + wrong = [B[1], B[2], B[3], B[0]] # rotr8 pattern used for 16 + s2 = Solver() + s2.add(w.word32(wrong) != RotateRight(w.word32(B), 16)) + return s2.check() == sat + bites("B", "relabel value check", tamper_relabel) + + # -- B2 range-check provenance: the load-bearing claim ------------------ + print("\n -- B2 where does each SSA word's byte range actually come from? --") + gcir, gv, _ = _build_one_g() + kinds = {} + for w in gcir.words: + kinds[w["kind"]] = kinds.get(w["kind"], 0) + 1 + record("B", "one G commits 56 byte-cells + 6 carry bits (DESIGN 3 table)", + sum(w["used"] for w in gcir.words + if w["kind"] in ("add_out", "xor_out", "sll", "sllc", "rot_out")) == 56 + and sum(1 for c in gcir.C if "Or" in str(c)[:3] or str(c).startswith("Or")) == 6, + f"cells={sum(w['used'] for w in gcir.words if w['kind'] not in ('input','unassigned','const'))}, " + f"bool-constraints={sum(1 for c in gcir.C if str(c).startswith('Or'))}") + record("B", "one G = 2 add3 + 2 add2 + 4 xor + 2 shift-rotations + " + "1 rotr16 + 1 rotr8 (DESIGN 2/5)", + [sum(1 for o in gcir.ops if o["kind"] == k) + for k in ("add3", "add2", "xor", "rotr", "relabel16", "relabel8")] + == [2, 2, 4, 2, 1, 1]) + + xc_g = gcir.xor_consumed() + add_outs = [wid for wid, w in enumerate(gcir.words) if w["kind"] == "add_out"] + record("B", "all FOUR add outputs of a G (A1, C1, A2, C2) are consumed by a " + "ByteAlu INSIDE the same G — so MAIN 0's byte declaration is " + "derivable for them (the class where the range check is " + "load-bearing; see section C)", + len(add_outs) == 4 and all(w in xc_g for w in add_outs)) + + unchecked_in_g = gcir.unchecked() + detail = ", ".join(f"w{wid}({gcir.words[wid]['kind']})" for wid in unchecked_in_g) + record("B", "INSIDE one G, exactly one SSA output has no ByteAlu consumer: " + "the final rotr7 result B2 (its range check lives in the NEXT " + "G / the feed-forward — outside MAIN 0's scope)", + len(unchecked_in_g) == 1 + and gcir.words[unchecked_in_g[0]]["kind"] == "rot_out" + and unchecked_in_g[0] == gcir.owner[str(gv[1][0])], + f"unchecked in G-scope: [{detail}]") + + ccir, cout, cin = _build_compress(6) + unchecked_full = ccir.unchecked() + record("B", "in the FULL 6-round compression every add/shift output IS " + "consumed by a ByteAlu[XOR] — DESIGN 5/7.4's premise verified " + "mechanically (the gate never checks it)", + unchecked_full == [], + f"{sum(1 for w in ccir.words if w['kind'] in ('add_out','rot_out'))} " + f"add/shift outputs, {len(unchecked_full)} unchecked") + other_rounds = {r: len(_build_compress(r)[0].unchecked()) for r in (1, 2, 7)} + record("B", "…and for ROUNDS = 1, 2 and 7 too, so the premise is a property " + "of the layout, not of the round count", + set(other_rounds.values()) == {0}, str(other_rounds)) + + xc = ccir.xor_consumed() + ins_xored = {k: all(ccir.owner[str(w[0])] in xc for w in + (cin[k] if isinstance(cin[k], list) and + isinstance(cin[k][0], list) else [cin[k]])) + for k in ("h", "m", "tlo", "thi", "bl", "fl")} + record("B", "DESIGN 4.7 input claim: h, t_lo, t_hi, block_len, flags each " + "feed an XOR; m does NOT (so m is the one input needing an " + "explicit AreBytes)", + ins_xored["h"] and ins_xored["tlo"] and ins_xored["thi"] + and ins_xored["bl"] and ins_xored["fl"] and not ins_xored["m"], + str(ins_xored)) + + # which XOR is h[i]'s range check? h[0..4] land in round-0 'a' slots, + # which G only ever uses as an ADD operand — so their sole ByteAlu is the + # UPPER feed-forward half, the half a CV-only caller (DESIGN 1.1) would + # naturally drop. + h_consumers = [] + for i in range(8): + wid = ccir.owner[str(cin["h"][i][0])] + cons = [oi for oi, o in enumerate(ccir.ops) + if o["kind"] == "xor" and any(wid in gp for gp in o["ins"])] + h_consumers.append(len(cons)) + ff_start = min(oi for oi, o in enumerate(ccir.ops) + if o["kind"] == "xor" and + any(ccir.owner[str(cin["h"][0][0])] in gp for gp in o["ins"])) + record("B", "h[0..4] have exactly ONE ByteAlu consumer (the upper " + "feed-forward out[i+8]=v[i+8]^h[i]) while h[4..8] have two " + "(they are round-0 'b' slots, so X2 xors them too)", + h_consumers == [1, 1, 1, 1, 2, 2, 2, 2], + f"consumer counts {h_consumers}") + record("B", "…and that single consumer really is a feed-forward XOR, not a " + "round XOR (it is among the last 24 ops of the circuit)", + ff_start >= len(ccir.ops) - 24, + f"op #{ff_start} of {len(ccir.ops)}") + + def tamper_provenance(): + """A G-variant in which an add output has NO ByteAlu consumer — + exactly the deviation DESIGN 7.4 warns about. The detector must see + it.""" + def build_g_leaky(cir, v, a, b, c, d, mx, my, bug, gflag): + v[a] = cir.add3(v[a], v[b], mx) + v[d] = cir.rotr16(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) + v[b] = cir.rotr(cir.xor(v[b], v[c]), 12) + v[a] = cir.add3(v[a], v[b], my) + v[d] = cir.rotr8(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) # C2: consumer removed below + v[b] = cir.rotr(cir.xor(v[b], v[a]), 7) # reads v[a], not v[c] + cir2, _, _ = _build_one_g(build=build_g_leaky) + return len(cir2.unchecked()) == 2 + bites("B", "range-check provenance detector", tamper_provenance) + + # -- B3 the message schedule as build_compress actually wires it ------- + print("\n -- B3 message indexing under permute^r --") + + def capture_wiring(rounds=7): + """What build_compress ACTUALLY feeds each G: the original message + column index, recovered by tagging the committed message words.""" + seen = [] + orig_bg = GATE.build_g + + def spy_g(cir, v, a, b, c, d, mx, my, bug, gflag): + seen.append((a, b, c, d, mx[0], my[0])) + return orig_bg(cir, v, a, b, c, d, mx, my, bug, gflag) + + GATE.build_g = spy_g + try: + cir3 = Traced("sched") + h3 = [cir3.fresh_word() for _ in range(8)] + m3 = [cir3.fresh_word() for _ in range(16)] + tg = {str(m3[i][0]): i for i in range(16)} + t3 = [cir3.fresh_word() for _ in range(4)] + GATE.build_compress(cir3, h3, m3, t3[0], t3[1], t3[2], t3[3], rounds) + finally: + GATE.build_g = orig_bg + wired = [[(tg.get(str(mx)), tg.get(str(my))) + for (_, _, _, _, mx, my) in seen[r * 8:(r + 1) * 8]] + for r in range(rounds)] + return wired, [q[:4] for q in seen] + + wired, quads = capture_wiring(7) + # what the ORACLE says round r must consume: permute^r applied to the + # identity schedule, then indexed by round_fn's own message positions + expected = [] + sched = list(range(16)) + for r in range(7): + expected.append([(sched[ix], sched[iy]) for (_, _, _, _, ix, iy) + in ora_G_CALLS()]) + sched = ORA.permute(sched) + record("B", "build_compress feeds every G the ORIGINAL message column " + "under permute^r, for all 7 rounds x 8 G-calls (56 index " + "pairs), matching the oracle's permutation composition", + wired == expected, + f"round0 {wired[0][:2]}... round6 {wired[6][:2]}...") + record("B", "the state quadruples build_round passes match the oracle's " + "round_fn quadruples in order, all 7 rounds", + quads == [tuple(c[:4]) for c in ora_G_CALLS()] * 7) + + def tamper_sched(): + old = GATE.MSG_PERMUTATION[:] + GATE.MSG_PERMUTATION[3], GATE.MSG_PERMUTATION[4] = old[4], old[3] + try: + w2, _ = capture_wiring(7) + finally: + GATE.MSG_PERMUTATION[:] = old + return w2 != expected + bites("B", "message-schedule index check", tamper_sched) + + def tamper_quads(): + old = GATE.G_CALLS[5] + GATE.G_CALLS[5] = (1, 6, 11, 12, 11, 10) # mx/my swapped + try: + w2, q2 = capture_wiring(7) + finally: + GATE.G_CALLS[5] = old + return w2 != expected + bites("B", "G-call wiring check", tamper_quads) + + # -- B4 what the model does NOT carry ---------------------------------- + print("\n -- B4 what the circuit model does not represent --") + src = open(os.path.join(HERE, "blake3-chip/z3_blake_verify.py")).read() + + # Every variable the model creates is 8 bits wide, and every constraint it + # emits is an equation or a carry booleanity. There is no range-check + # OBJECT, so "AreBytes present" and "AreBytes absent" are the same model. + widths = set() + for w in ccir.words: + widths.update(c.size() for c in w["cells"]) + kinds = {} + for c in ccir.C: + kinds[c.decl().name()] = kinds.get(c.decl().name(), 0) + 1 + record("B", "every committed cell the model creates is BitVec(...,8): " + "byte-ness is the DECLARATION, never a modelled lookup", + widths == {8}, f"cell widths {sorted(widths)}") + record("B", "and every emitted constraint is '=' (xor / sum / shift / " + "recombine) or 'or' (carry booleanity) — no range constraint " + "object exists to be present or absent", + set(kinds) <= {"=", "or"}, str(kinds)) + cls_body = src[src.index("# Chip circuit model"):src.index("def build_g")] + record("B", "inside the Circuit class, 'AreBytes' occurs only in comments " + "(the class header ':118-119' and rotr ':212') — the gate " + "DOCUMENTS the assumption ('Byte width == the ByteAlu/AreBytes " + "range-check contract') but has no object for it", + all(ln.strip().startswith("#") for ln in cls_body.split("\n") + if "AreBytes" in ln)) + record("B", "the model has NO mu column: every eval identity is asserted " + "ungated, which is exact for a live row and blind to padding " + "rows (DESIGN 4.5 / 7.1 are therefore outside the gate)", + not any("mu" in str(c).lower() for c in ccir.C) + and "mu" not in "".join(str(w["cells"][0]) for w in ccir.words)) + record("B", "the model has no bus / multiplicity / timestamp layer at all " + "(confirming README finding 1, not re-deriving it)", + not any(k in src for k in ("Multiplicity", "TIMESTAMP", "bus_interaction", + "receive(", "send("))) + nunused = sum(1 for w in ccir.words if w["kind"] in ("sll", "sllc")) + record("B", "each shift-rotation allocates 4 x fresh_word() but uses only " + "2 bytes of each (fresh_word()[:2]) — 8 free unconstrained BVs " + "per rotation, unread and harmless", + nunused == 4 * 96, f"{nunused} halfword slots over 96 rotations") + + +# =========================================================================== +# SECTION C — the dangerous direction, in the field: where byte-ness +# actually comes from, and the forgery the model cannot see. +# =========================================================================== +def _field_word(s, name, ranged): + cells = [Int(f"{name}_{i}") for i in range(4)] + for c in cells: + s.add(c >= 0, c < (256 if ranged else P)) + return cells, sum(cells[i] * 2**(8 * i) for i in range(4)) + + +def add_pinned(nops, out_ranged, ops_concrete=None, want_model=False): + """DESIGN 4.3/4.4 add, modelled in the FIELD. Is the committed sum word + pinned to (sum of operands) mod 2^32? unsat = pinned, sat = forgeable.""" + s = Solver() + ops = [] + for k in range(nops): + if ops_concrete: + ops.append(IntVal(ops_concrete[k])) + else: + _, v = _field_word(s, f"in{k}", True) + ops.append(v) + scells, S = _field_word(s, "S", out_ranged) + if nops == 2: + c = Int("c") + s.add(Or(c == 0, c == 1)) + csum = c + else: + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + csum = c1 + c2 + s.add((sum(ops) - S - 2**32 * csum) % P == 0) + T, K = Int("T"), Int("K") + s.add(K >= 0, K <= nops - 1, T >= 0, T < 2**32, sum(ops) == K * 2**32 + T) + s.add((S - T) % P != 0) # a wrong FIELD VALUE, not just cells + res = s.check() + if res == sat and want_model: + mo = s.model() + g = lambda e: mo.eval(e, model_completion=True).as_long() + return res, dict(operands=[hex(g(o)) for o in ops], honest=hex(g(T)), + forged=hex(g(S) % P), cells=[g(x) for x in scells], + carries=g(csum)) + return res, None + + +def rot_pinned(r, kept, want_model=False): + """DESIGN 4.2 rotation in the FIELD, COMPOSED: both shift identities + + both recombine identities + the byte range on Y that the downstream + ByteAlu gives. `kept` = which halfwords carry their AreBytes bound.""" + s = Solver() + xlo, xhi = Int("xlo"), Int("xhi") + s.add(xlo >= 0, xlo < 2**16, xhi >= 0, xhi < 2**16) + hw = {} + for n in ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"): + if n in kept: + lo, hi = Int(n + "_b0"), Int(n + "_b1") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + hw[n] = lo + 256 * hi + else: + v = Int(n) + s.add(v >= 0, v < P) + hw[n] = v + s.add((xlo * 2**r - hw["SLLC_lo"] * 2**16 - hw["SLL_lo"]) % P == 0) + s.add((xhi * 2**r - hw["SLLC_hi"] * 2**16 - hw["SLL_hi"]) % P == 0) + Y = [Int(f"Y{i}") for i in range(4)] + for y in Y: + s.add(y >= 0, y < 256) + Ylo, Yhi = Y[0] + 256 * Y[1], Y[2] + 256 * Y[3] + s.add((Ylo - hw["SLL_hi"] - hw["SLLC_lo"]) % P == 0) + s.add((Yhi - hw["SLL_lo"] - hw["SLLC_hi"]) % P == 0) + X = xlo + 2**16 * xhi + Q, R = Int("Q"), Int("R") + s.add(Q >= 0, Q < 2**r, R >= 0, R < 2**32, X * 2**r == Q * 2**32 + R) + wlo, whi = Int("wlo"), Int("whi") + s.add(wlo >= 0, wlo < 2**16, whi >= 0, whi < 2**16, R + Q == wlo + 2**16 * whi) + honest = whi + 2**16 * wlo + s.push() + s.add(Ylo + 2**16 * Yhi != honest) + res = s.check() + mdl = None + if res == sat and want_model: + mo = s.model() + g = lambda e: mo.eval(e, model_completion=True).as_long() + mdl = dict(X=hex(g(X)), honest_Y=hex(g(honest)), + forged_Y=hex(g(Ylo + 2**16 * Yhi)), + SLL_lo=hex(g(hw["SLL_lo"])), SLLC_lo=hex(g(hw["SLLC_lo"])), + SLL_hi=hex(g(hw["SLL_hi"])), SLLC_hi=hex(g(hw["SLLC_hi"]))) + s.pop() + # non-vacuity: the honest witness must satisfy the model + s.add(Ylo + 2**16 * Yhi == honest) + live = s.check() == sat + return res, mdl, live + + +def section_C(slow): + print("\n" + "=" * 74) + print("C FIELD-LEVEL — what the byte range checks actually buy, and the " + "forgery the\n BV model cannot express") + print("=" * 74) + + for n in (2, 3): + res, _ = add_pinned(n, True) + record("C", f"add{n}: WITH the output's byte range check the sum is " + f"pinned to (a+b{'+m' if n == 3 else ''}) mod 2^32, for " + f"ALL operands (symbolic, mod p)", res == unsat) + for n in (2, 3): + res, mdl = add_pinned(n, False, want_model=True) + record("C", f"add{n}: WITHOUT it the committed sum is FORGEABLE — the " + f"prover commits the UNREDUCED sum with carry 0", + res == sat, str(mdl)) + + res, mdl = add_pinned(2, False, ops_concrete=[0x80000000, 0x80000000], + want_model=True) + record("C", "concrete witness: a=b=0x80000000, honest s=0, forged s=2^32 " + "with carry=0 (cells [2^32,0,0,0]) — every modelled constraint " + "satisfied", res == sat and mdl["forged"] == hex(2**32), str(mdl)) + + # the gate cannot tell the two apart: its `s` is 4 BitVec(8)s either way + src = open(os.path.join(HERE, "blake3-chip/z3_blake_verify.py")).read() + record("C", "…and the gate models BOTH chips identically: add2/add3 return " + "`self.fresh_word()`, i.e. 4x BitVec(...,8), so the range check " + "is DECLARED, never derived from a modelled lookup", + "s = self.fresh_word()" in src and "AreBytes" not in + src[src.index("def add2"):src.index("def rotr")]) + + print("\n -- C2 the rotation, composed (the gate tests it in isolation) --") + lattice = {} + for r in (4, 9): + for k in range(4, -1, -1): + for kept in itertools.combinations( + ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"), k): + res, _, live = rot_pinned(r, set(kept)) + lattice[(r, kept)] = (res, live) + all_live = all(live for _, live in lattice.values()) + record("C", "non-vacuity: the honest rotation witness satisfies the " + "composed field model in all 32 bound configurations", + all_live) + record("C", "rotation with all four AreBytes bounds: Y is pinned to " + "rotr12/rotr7 for ALL 2^32 inputs (symbolic, mod p — the gate " + "only ever checked one concrete halfword in the field)", + lattice[(4, ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"))][0] == unsat + and lattice[(9, ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"))][0] == unsat) + one_sll = all(lattice[(r, k)][0] == unsat for r in (4, 9) + for k in (("SLL_lo",), ("SLL_hi",))) + no_sll = all(lattice[(r, k)][0] == sat for r in (4, 9) + for k in ((), ("SLLC_lo",), ("SLLC_hi",), ("SLLC_lo", "SLLC_hi"))) + record("C", "necessary AND sufficient bound set = at least one of " + "{SLL_lo, SLL_hi}; every configuration with neither is " + "forgeable, every configuration with either is pinned — the " + "SLLC bounds are not load-bearing at all", + one_sll and no_sll) + res, mdl, _ = rot_pinned(9, set(("SLLC_lo", "SLLC_hi")), want_model=True) + record("C", "the composed rotation forgery (both SLL bounds dropped) exists " + "for exactly ONE input, X=0xFFFFFFFF -> forged Y=0 instead of " + "0xFFFFFFFF — not 'any input', as the gate's isolated control " + "suggests", res == sat and mdl["X"] == hex(0xFFFFFFFF), str(mdl)) + + # exhaustively: is X = 0xFFFFFFFF the only one? + def enumerate_bad_X(r, limit=4): + found = [] + seen = set() + for _ in range(limit): + s = Solver() + xlo, xhi = Int("xlo"), Int("xhi") + s.add(xlo >= 0, xlo < 2**16, xhi >= 0, xhi < 2**16) + hw = {} + for n in ("SLL_lo", "SLL_hi"): + v = Int(n) + s.add(v >= 0, v < P) + hw[n] = v + for n in ("SLLC_lo", "SLLC_hi"): + lo, hi = Int(n + "_b0"), Int(n + "_b1") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + hw[n] = lo + 256 * hi + s.add((xlo * 2**r - hw["SLLC_lo"] * 2**16 - hw["SLL_lo"]) % P == 0) + s.add((xhi * 2**r - hw["SLLC_hi"] * 2**16 - hw["SLL_hi"]) % P == 0) + Y = [Int(f"Y{i}") for i in range(4)] + for y in Y: + s.add(y >= 0, y < 256) + Ylo, Yhi = Y[0] + 256 * Y[1], Y[2] + 256 * Y[3] + s.add((Ylo - hw["SLL_hi"] - hw["SLLC_lo"]) % P == 0) + s.add((Yhi - hw["SLL_lo"] - hw["SLLC_hi"]) % P == 0) + X = xlo + 2**16 * xhi + Q, R = Int("Q"), Int("R") + s.add(Q >= 0, Q < 2**r, R >= 0, R < 2**32, X * 2**r == Q * 2**32 + R) + wlo, whi = Int("wlo"), Int("whi") + s.add(wlo >= 0, wlo < 2**16, whi >= 0, whi < 2**16, + R + Q == wlo + 2**16 * whi) + s.add(Ylo + 2**16 * Yhi != whi + 2**16 * wlo) + for x in seen: + s.add(X != x) + if s.check() != sat: + break + xv = s.model().eval(X, model_completion=True).as_long() + seen.add(xv) + found.append(hex(xv)) + return found + bad4, bad9 = enumerate_bad_X(4), enumerate_bad_X(9) + record("C", "exhaustive: X=0xFFFFFFFF is the ONLY forgeable input for both " + "r=4 and r=9 (all other X enumerated away -> unsat)", + bad4 == bad9 == ["0xffffffff"], f"r=4 {bad4} r=9 {bad9}") + + # …but the rotation OUTPUT does not need its own byte range check: both + # recombine identities together pin its VALUE regardless of how its cells + # decompose. So the free-range-check argument is load-bearing for the add + # outputs and for one SLL per rotation — and for nothing else. + s = Solver() + xlo, xhi = Int("xlo"), Int("xhi") + s.add(xlo >= 0, xlo < 2**16, xhi >= 0, xhi < 2**16) + hw = {} + for n in ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"): + lo, hi = Int(n + "_b0"), Int(n + "_b1") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + hw[n] = lo + 256 * hi + r = 9 + s.add((xlo * 2**r - hw["SLLC_lo"] * 2**16 - hw["SLL_lo"]) % P == 0) + s.add((xhi * 2**r - hw["SLLC_hi"] * 2**16 - hw["SLL_hi"]) % P == 0) + Ycells = [Int(f"Yf{i}") for i in range(4)] + for c in Ycells: + s.add(c >= 0, c < P) # NO range check on Y + Ylo, Yhi = Ycells[0] + 256 * Ycells[1], Ycells[2] + 256 * Ycells[3] + s.add((Ylo - hw["SLL_hi"] - hw["SLLC_lo"]) % P == 0) + s.add((Yhi - hw["SLL_lo"] - hw["SLLC_hi"]) % P == 0) + X = xlo + 2**16 * xhi + Q, R = Int("Q"), Int("R") + s.add(Q >= 0, Q < 2**r, R >= 0, R < 2**32, X * 2**r == Q * 2**32 + R) + wlo, whi = Int("wlo"), Int("whi") + s.add(wlo >= 0, wlo < 2**16, whi >= 0, whi < 2**16, R + Q == wlo + 2**16 * whi) + s.add((Ylo + 2**16 * Yhi - (whi + 2**16 * wlo)) % P != 0) + record("C", "the rotation OUTPUT needs no range check of its own: the two " + "recombine identities pin its value even with free field cells " + "— so the 'free range check' is load-bearing only for the add " + "outputs and one SLL halfword per rotation", s.check() == unsat) + + print("\n -- C3 the width audit's two claims, re-derived symbolically --") + # the gate proves each on ONE concrete input; prove them for all inputs + s = Solver() + inhw = Int("in_hw") + s.add(inhw >= 0, inhw < 2**16) + lo, hi = Int("lo"), Int("hi") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + SLL = lo + 256 * hi + SLLC = Int("SLLC") + s.add(SLLC >= 0, SLLC < 2**16) + r = 9 + s.add((inhw * 2**r - SLLC * 2**16 - SLL) % P == 0) + ref = Int("ref") + s.add(ref >= 0, ref < 2**16, (inhw * 2**r - ref) % 2**16 == 0) + s.add(SLL != ref) + record("C", "field_shift_bound's UNSAT holds for ALL in_hw, not just " + "0x9C3A (the gate tests one point)", s.check() == unsat) + + s = Solver() + a, b, m3 = Int("a"), Int("b"), Int("m") + for x in (a, b, m3): + s.add(x >= 0, x < 2**32) + S = Int("S") + s.add(S >= 0, S < 2**32) + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + s.add((a + b + m3 - S - 2**32 * (c1 + c2)) % P == 0) + K, T = Int("K"), Int("T") + s.add(K >= 0, K <= 2, T >= 0, T < 2**32, a + b + m3 == K * 2**32 + T) + s.add(S != T) + record("C", "field_add_carry's UNSAT holds for ALL (a,b,m), not just " + "3x0xF0000000", s.check() == unsat) + + # dropping the booleanity really does free s, composed with s's byte range + s = Solver() + a, b, m3 = IntVal(0x12345678), IntVal(0x9ABCDEF0), IntVal(0x0F0F0F0F) + scells, S = _field_word(s, "S", True) # s STILL byte-range-checked + k = Int("k") + s.add(k >= 0, k < P) # booleanity dropped + s.add((a + b + m3 - S - 2**32 * k) % P == 0) + honest = (0x12345678 + 0x9ABCDEF0 + 0x0F0F0F0F) % 2**32 + s.add(S != honest) + res = s.check() + mdl = None + if res == sat: + mo = s.model() + mdl = dict(honest=hex(honest), + forged=hex(mo.eval(S, model_completion=True).as_long()), + k=mo.eval(k, model_completion=True).as_long()) + record("C", "dropping the carry booleanity is forgeable even WITH the byte " + "range check on s (so this control is faithful to the composed " + "chip, unlike the shift one)", res == sat, str(mdl)) + + print("\n -- C4 the message columns (DESIGN 4.7): AreBytes vs the model --") + # without AreBytes on m the cells bind only sum(m_i 2^8i): exhibit two + # distinct cell vectors that satisfy every constraint identically. + honest_cells = [0x9A, 0x00, 0x13, 0x7F] + forged_cells = [0x9A + 256, 0x00 - 1, 0x13, 0x7F] + same_value = (sum(honest_cells[i] * 2**(8 * i) for i in range(4)) % P == + sum(forged_cells[i] * 2**(8 * i) for i in range(4)) % P) + record("C", "without the explicit AreBytes, a message word has many cell " + "representations with the same value (here [0x9A,0,0x13,0x7F] " + "and [0x19A,-1,0x13,0x7F] = [.., p-1, ..]): the chip binds " + "sum(m_i 2^8i), not the 64 bytes", same_value, + f"forged cells over F_p: {[c % P for c in forged_cells]}") + record("C", "the gate declares m as 16 x 4 BitVec(...,8), so it proves the " + "SAME UNSAT for a chip with and without those 32 AreBytes sends", + "m = [cir.fresh_word() for _ in range(16)]" in src) + + +# =========================================================================== +# SECTION D — gate hygiene: are the UNSATs non-vacuous, and is the model's +# carry encoding the one DESIGN.md specifies? +# =========================================================================== +def section_D(slow): + print("\n" + "=" * 74) + print("D GATE HYGIENE") + print("=" * 74) + + cir, v, _ = _build_one_g() + s = Solver() + s.add(And(*cir.C)) + record("D", "the G circuit's constraint set is SATISFIABLE on its own — " + "MAIN 0's UNSAT is not vacuous", s.check() == sat) + counts = {} + for opname, call in (("xor", lambda c: c.xor(c.fresh_word(), c.fresh_word())), + ("add2", lambda c: c.add2(c.fresh_word(), c.fresh_word())), + ("add3", lambda c: c.add3(c.fresh_word(), c.fresh_word(), + c.fresh_word())), + ("rotr12", lambda c: c.rotr(c.fresh_word(), 12)), + ("rotr16", lambda c: c.rotr16(c.fresh_word()))): + c = GATE.Circuit("cnt") + call(c) + counts[opname] = len(c.C) + record("D", "constraint counts per op match DESIGN 4.1-4.4: xor 4 (pure " + "lookup, modelled as 4 byte equalities), add2 2 (sum + 1 " + "booleanity), add3 3 (sum + 2 booleanities), rotr12 4 " + "(2 shift + 2 recombine), rotr16 0 (free relabel)", + counts == {"xor": 4, "add2": 2, "add3": 3, "rotr12": 4, "rotr16": 0}, + str(counts)) + + ccir, cout, _ = _build_compress(6) + s = Solver() + s.add(And(*ccir.C)) + record("D", "the 6-round circuit's constraint set is SATISFIABLE on its own", + s.check() == sat) + + # DESIGN 4.3 commits NO carry column for the 2-op add (carry is the linear + # expression (a+b-s)*2^-32); the model commits a boolean column instead. + # Prove the two are equivalent. + s = Solver() + A, B, S = Int("A"), Int("B"), Int("S") + for x in (A, B, S): + s.add(x >= 0, x < 2**32) + c_derived = Int("cd") + lhs = Or(And((A + B - S - 2**32 * 0) % P == 0), + And((A + B - S - 2**32 * 1) % P == 0)) # committed-boolean form + rhs = ((A + B - S) * pow(2**32, -1, P) % P == 0) + # derived form: carry := (A+B-S)*2^-32 mod p, booleanity carry*(carry-1)=0 + cd = ((A + B - S) * pow(2**32, -1, P)) % P + rhs = Or(cd == 0, cd == 1) + s.add(lhs != rhs) + record("D", "DESIGN 4.3's DERIVED carry (linear expr x INV_SHIFT_32, " + "booleanity) and the model's COMMITTED boolean carry are " + "equivalent over F_p — the difference is 1 column per add2, " + "not a semantic one", s.check() == unsat) + + note("DESIGN 4.8's ledger row for the recombine identity says body degree " + "2 -> 3 after x mu; the body mu*(Ylo - SLL_hi - SLLC_lo) is LINEAR in " + "committed columns, so it is 1 -> 2. Over-stated in the safe " + "direction; the 'no constraint exceeds 3' verdict is unaffected.") + note("DESIGN 3's per-G table counts 1 committed carry bit for each add2, " + "while DESIGN 4.3 makes that carry a DERIVED linear expression " + "(a+b-s)*INV_SHIFT_32 with no column. The gate models the committed " + "form. Equivalent as constraints (proved above); the two readings " + "differ by 96 cells/compression in the DESIGN 6 cost table.") + + # The positive controls are the gate's only external anchor. They pin the + # circuit's output to a RECORDED vector, so a stale fixture would silently + # anchor the gate to nothing. + vecs = GATE.load_canonical_6round() + ok_vec = all(ORA.compress_6round(v["h"], v["m"], v["t"], v["block_len"], + v["flags"]) == v["out"] for v in vecs) + record("D", f"all {len(vecs)} canonical 6-round fixture vectors reproduce " + "from the oracle's compress_6round — the positive controls " + "anchor to the live oracle, not a stale file", ok_vec) + record("D", "…and they are genuinely 6-round: none of them equals the " + "7-round compression of the same input", + all(ORA.compress(v["h"], v["m"], v["t"], v["block_len"], v["flags"], + rounds=7) != v["out"] for v in vecs)) + h7, m7, tlo7, thi7, bl7, fl7, out7 = GATE.gen_7round_vector() + record("D", "gen_7round_vector's output is the oracle's 7-round " + "compression of its own inputs", + ORA.compress(h7, m7, tlo7 | (thi7 << 32), bl7, fl7, rounds=7) == out7) + + # WIDE = 48 must be wide enough that the BV identities are integer + # identities, and small-enough values that they coincide with mod-p. + s = Solver() + a, b, m3, S = Int("a"), Int("b"), Int("m"), Int("S") + for x in (a, b, m3, S): + s.add(x >= 0, x < 2**32) + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + s.add((a + b + m3 == S + 2**32 * (c1 + c2)) != + (((a + b + m3 - S - 2**32 * (c1 + c2)) % P) == 0)) + record("D", "the 3-op sum identity over Z (what WIDE=48 BV computes) and " + "over F_p (what the chip computes) are equivalent under the " + "byte bounds — no wraparound is available on either side, " + "confirming DESIGN 7.9", s.check() == unsat) + # The gate ASSUMES the BITWISE contracts (assume-guarantee). They are + # cheap to verify against the real table, so verify them. + bw = os.path.join(HERE, "..", "..", "prover", "src", "tables", "bitwise.rs") + if os.path.exists(bw): + rs = open(bw).read() + record("D", "the assumed ByteAlu[XOR] contract is real: bitwise.rs " + "enumerates x,y in 0..256 and sets cols::XOR = x^y, and the " + "receiver pins (XOR, X, Y) -> XOR", + "for x in 0u32..256 {" in rs and "for y in 0u32..256 {" in rs + and "table.set_byte(row_idx, cols::XOR, (x ^ y) as u8);" in rs + and "Multiplicity::Column(cols::MU_BYTE_ALU_XOR)" in rs) + record("D", "the assumed AreBytes contract is real: an AreBytes " + "receiver over the same 0..256 x 0..256 domain", + "BusId::AreBytes," in rs + and "ARE_BYTES[X, Y] - range check two byte values" in rs) + else: + note(f"bitwise.rs not found at {bw}; the ByteAlu/AreBytes contracts " + "were not cross-checked in this run.") + + note("block_len and flags are modelled as free 32-bit words; the design " + "says 0..64 and 0..127. The model is WEAKER there, which is the safe " + "direction, and DESIGN.md specifies no such constraint either.") + note("DESIGN 4.1 allows a ByteAlu operand to be a linear combination of " + "cells ('sum <= 255'); the model never uses one — rotr16/rotr8 are " + "pure index relabels, so every operand is a single cell. Consistent, " + "but the linear-combo contract is therefore unexercised.") + + if slow: + print("\n -- D2 the gate's own BV verdicts, re-run --") + record("D", "check_g() == unsat (MAIN 0)", GATE.check_g() == unsat) + record("D", "check_compress(0) == unsat (MAIN 1)", + GATE.check_compress(0) == unsat) + record("D", "check_g(bug='swap_g_operand') == sat", + GATE.check_g(bug="swap_g_operand") == sat) + + +# =========================================================================== +def main(): + slow = "--slow" in sys.argv + print("=" * 74) + print("BLAKE3 GATE TRANSCRIPTION AUDIT — regression suite") + print("=" * 74) + section_A(slow) + section_B(slow) + section_C(slow) + section_D(slow) + + print("\n" + "=" * 74) + fails = [(s, n) for s, n, ok, _ in RESULTS if not ok] + print(f"SUMMARY: {len(RESULTS) - len(fails)}/{len(RESULTS)} checks pass") + for s, n in fails: + print(f" FAIL [{s}] {n}") + print("=" * 74) + sys.exit(1 if fails else 0) + + +if __name__ == "__main__": + main() diff --git a/thoughts/blake3/blake3-chip/DESIGN.md b/thoughts/blake3/blake3-chip/DESIGN.md index 8aec428d3..1a694740d 100644 --- a/thoughts/blake3/blake3-chip/DESIGN.md +++ b/thoughts/blake3/blake3-chip/DESIGN.md @@ -202,6 +202,23 @@ read in-place. Eval constraints: none (pure lookup). Degree: n/a. recombination sums are over non-overlapping bit ranges, so `+` = `OR` and each is an exact 16-bit halfword. + **Refined by the transcription audits (2026-07-29), symbolically over all + 2^32 inputs — the earlier wording was coarser than the truth:** + * The load-bearing bound set is **at least one of `{SLL_lo, SLL_hi}`**. Every + configuration with neither is forgeable; every configuration with either is + pinned. **The two `SLLC` bounds are not load-bearing at all** — so of the 4 + `AreBytes` sends per rotation, only the `SLL` pair carries soundness weight. + Read the sentence above as "a tight bound on at least one `SLL` halfword", + not "the tight `SLL` bound". + * The *composed* forgery (both `SLL` bounds dropped) exists for exactly **one** + input, `X = 0xFFFFFFFF` → forged `Y = 0`, exhaustively confirmed for both + `r = 4` and `r = 9`. The gate's isolated control makes it look reachable for + arbitrary inputs; it is not. Narrow, but a forgery is a forgery. + * The rotation **output** needs no range check of its own: the two recombine + identities pin its value even with free field cells. So the §4.7 "free range + check" argument is load-bearing for the **add** outputs and one `SLL` + halfword per rotation — not for the rotation output. + **HWSL alternative, priced:** replace each shift identity with an `Hwsl` send (`bitwise.rs:831`). Cost/rotation: +2 Hwsl sends, same AreBytes, same columns. Per compression that is +4 sends/G × 48 = +192 sends → +288 aux cells (≈6%). @@ -282,7 +299,7 @@ output feeds a downstream XOR ⇒ free. | 3-op add sum identity | 1 | 2 | ✅ | | 3-op add carry booleanity ×2 | 2 | 3 | ✅ | | shift identity (×2) | 1 | 2 | ✅ | -| recombine identity (×2) | 2 | 3 | ✅ | +| recombine identity (×2) | 1 (was stated as 2) | 2 | ✅ | | (rejected) ternary carry | 3 | **4** | ❌ | Worst legal constraint = 3. **No constraint exceeds 3.** @@ -335,14 +352,31 @@ oracle's prose "¼–⅓ of a keccak permutation" is inconsistent with its own 2. **3-op add = two summed carry bits with the explicit sum identity** — not a single ternary carry (degree 4 after gating), and the sum identity must be present (without it, `s` is only constrained mod nothing). (§4.4) -3. **Shift identity needs the tight `SLL ∈ [0,2^16)` AreBytes bound**; dropping it +3. **Shift identity needs a tight `∈ [0,2^16)` AreBytes bound on at least one of + `SLL_lo`/`SLL_hi`** (the `SLLC` bounds are *not* load-bearing — audited + 2026-07-29, §4.2); dropping it makes the rotation forgeable (a wrong `SLL` admits a large field `SLLC`). Soundness relies on `2^16` invertible mod p — a BV model cannot see this; verify in the field (gate width audit + `hwsl_inline_test.py`). (§4.2) -4. **Every add/shift output must actually feed a downstream XOR** (its only range +4. **Every add output must actually feed a downstream XOR** (its only range check). If a future refactor reorders so an add output is *last* with no XOR consumer, add an explicit AreBytes or the carry argument is unsound. (§5) + + ⚠ **THE GATE CANNOT CHECK THIS, and both 2026-07-29 audits confirmed it with + explicit forgeries.** `build_g` returns each add output as `fresh_word()` = + 4×`BitVec(...,8)`, so byte range is **declared by construction, never derived + from a modelled lookup**. The gate therefore proves the identical UNSAT for a + chip that has the downstream XOR and for one that does not. Drop it and the + sum is forgeable — witness `a = b = 0x80000000`, honest `s = 0`, forged + `s = 2^32` with `carry = 0`, satisfying every modelled constraint. This + invariant rests entirely on the implementer, and a green board is not + evidence for it. 5. **Message `m` needs explicit AreBytes** — it is never XORed. (§4.7) + ⚠ Same blind spot: the gate declares `m` as 16×4 `BitVec(...,8)`, so it proves + the same UNSAT with or without those 32 `AreBytes` sends. Without them a + message word has many cell representations of one value over `F_p` (e.g. + `[0x9A,0,0x13,0x7F]` and `[0x19A,p−1,0x13,0x7F]`), because the chip binds + `Σ m_i·2^(8i)`, not the 64 bytes. 6. **rotr16/rotr8 byte order** exactly `[b2,b3,b0,b1]` / `[b1,b2,b3,b0]` (little-endian). A wrong relabel silently corrupts. (§4.2) 7. **Message permutation `permute^r`** wired per round from the *original* 16 diff --git a/thoughts/blake3/ground-truth/Cargo.toml b/thoughts/blake3/ground-truth/Cargo.toml index 4e8de39f4..01cc4bf46 100644 --- a/thoughts/blake3/ground-truth/Cargo.toml +++ b/thoughts/blake3/ground-truth/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "gt" version = "0.1.0" diff --git a/thoughts/blake3/ground-truth/src/bin/counter_probe.rs b/thoughts/blake3/ground-truth/src/bin/counter_probe.rs new file mode 100644 index 000000000..ac3dadff7 --- /dev/null +++ b/thoughts/blake3/ground-truth/src/bin/counter_probe.rs @@ -0,0 +1,32 @@ +// SCRATCH (audit, not committed): probe the XOF counter path of the official +// blake3 crate. For a fixed single-block input, the root output block at +// counter t is compress(key, block, t, block_len, flags|ROOT) — so seeking an +// OutputReader to byte position t*64 exercises the v[12]/v[13] counter split +// at arbitrary t, including t >= 2^32. +use blake3::Hasher; +use std::io::Write; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +fn main() { + // 64-byte single-block input, same pattern as the Python side. + let input: Vec = (0..64).map(|i| (i % 251) as u8).collect(); + let counters: Vec = vec![ + 0, 1, 2, 0xFFFF_FFFE, 0xFFFF_FFFF, 0x1_0000_0000, 0x1_0000_0001, + 0x100_0000_0000, // 2^40 + 0x8000_0000_0000, // 2^47 + ]; + let stdout = std::io::stdout(); + let mut w = std::io::BufWriter::new(stdout.lock()); + for &t in &counters { + let mut h = Hasher::new(); + h.update(&input); + let mut reader = h.finalize_xof(); + reader.set_position(t * 64); + let mut out = [0u8; 64]; + reader.fill(&mut out); + writeln!(w, "{} {}", t, hex(&out)).unwrap(); + } +} From 820fe7f1bf1beefe75d73df8656dbe6e3880fb7e Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 29 Jul 2026 18:01:23 -0300 Subject: [PATCH 06/14] docs(blake3): mark the dead ../keccak-verify citations as historical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESIGN.md defers twice to `../keccak-verify/` — the cost model (tier2_cost_model.md) and the shift-identity bound-necessity proof (hwsl_inline_test.py Part 2). That directory died in the same 2026-07-23 session scratchpad this design was recovered from and was never committed: it exists on no branch, and `git log --all --diff-filter=A` finds it zero times. A reader following either citation chases nothing. Nothing rests on them. The cost arithmetic was recomputed from scratch (per-G 62 cells x 48 = 2,976; columns 3,155; sends 1,250; aux 1,875; total ~5,030, and ~1/15 of keccak-f) and checks out. The shift-identity result was re-derived symbolically over all 2^32 inputs by the 2026-07-29 transcription audits, which is stronger than the single-point check the lost file made — one audit flagged the missing file precisely because it had re-derived the result rather than trusting the citation. Fourth dead-citation instance this week. The others: lincomb2's "blake3 6-round assumption" precedent, which never existed because this very work never shipped; the EC gate's C5 pointing at a document containing no soundness argument; and the EC board's C4 invoked well outside its own text. Marking rather than deleting, so the provenance stays legible. Gate re-run after the edit: OVERALL PASS. --- thoughts/blake3/blake3-chip/DESIGN.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/thoughts/blake3/blake3-chip/DESIGN.md b/thoughts/blake3/blake3-chip/DESIGN.md index 1a694740d..9a04d72f5 100644 --- a/thoughts/blake3/blake3-chip/DESIGN.md +++ b/thoughts/blake3/blake3-chip/DESIGN.md @@ -6,6 +6,20 @@ external anchors). Cost model = the verified one in `../keccak-verify/tier2_cost (a committed cell is expensive; each bus send ≈ 1.5 base cells of aux; **hard** max constraint degree 3 *including* the ×μ gating factor). +> **⚠ Every `../keccak-verify/` citation in this document is DEAD.** That +> directory lived in the same 2026-07-23 session scratchpad this design was +> recovered from and was never committed — it exists on no branch. Two +> conclusions were deferred to it, and both have since been re-established +> independently, so nothing here rests on the missing files: +> * the **cost model** above (1.5 aux cells/send, degree ≤ 3 incl. ×μ) — the +> per-G and per-compression arithmetic in §2/§3/§6 was recomputed from +> scratch and checks out; +> * the **shift-identity bound necessity** cited at §4.2 and §9 — re-derived +> symbolically over all 2^32 inputs by the 2026-07-29 transcription audits, +> which is stronger than the single-point check the lost file made. +> +> Do not go looking for them; read the citations as historical. + **Verdict (numbers derived below, gate in `z3_blake_verify.py`):** * **Layout: B — one row per compression, fully unrolled.** Chosen by arithmetic (≈5,030 cell-equiv vs ≈5,510 for one-row-per-round), and it deletes the @@ -194,8 +208,10 @@ read in-place. Eval constraints: none (pure lookup). Degree: n/a. SLLC_hi` = 4 sends/rotation (`bitwise.rs:783`). `Y` is range-checked *free* by the downstream XOR that consumes it. - Soundness (proven in `../keccak-verify/hwsl_inline_test.py` Part 2, and by the - width audit in the gate): given `SLL_* ∈ [0,2^16)` (the tight remainder bound + Soundness (originally deferred to `../keccak-verify/hwsl_inline_test.py` + Part 2 — **that file is lost, see the banner at the top; the result was + re-derived independently and more strongly by the 2026-07-29 audits** — and by + the width audit in the gate): given `SLL_* ∈ [0,2^16)` (the tight remainder bound from AreBytes) and `2^16` invertible mod p, the identity **uniquely** pins `SLL = (xlo·2^r) mod 2^16` and `SLLC = (xlo·2^r) >> 16`; the loose 16-bit bound on `SLLC` suffices because it is the quotient, not the remainder. The two From 35038501c65f5efc1deb9f733164227ade3d0cb8 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 5 Aug 2026 13:29:07 -0300 Subject: [PATCH 07/14] feat(prover,executor): BLAKE3 6-round compression accelerator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Rust realization of the gate-proved design in thoughts/blake3/ (recovered spike, merged into this branch): syscall u64::MAX-2, executor implementation ported from the validated oracle, and a one-row-per-compression chip. ⚠ 6-round INTERNAL variant, not standard 7-round BLAKE3. Merkle/Fiat-Shamir replacement candidate; rests on the named A6R assumption recorded in thoughts/blake3/blake3-chip/IMPLEMENTATION.md, to be ratified in the spec before production use. ABI: x10 -> 8-aligned 176-byte region, h[32] | m[64] | t[8] | len,flags[8] | out[64]; the syscall writes the 64-byte output back. One compression digests a 64-byte message block — exactly one 2-to-1 Merkle merge of two 32-byte CVs. Chip (prover/src/tables/blake3.rs): fully unrolled Layout B — 3,219 main columns, 1,397 sends, 813 mu-gated constraints, max degree 3, ~5,316 cell-equiv per compression ~= 1/13.7 of a post-#889 keccak-f. The compression dataflow is written once (run_flow) and interpreted twice — columns (constraints + bus senders) and u32 values (trace fill + BITWISE multiplicities) — so sender/witness wiring cannot diverge by construction. Design decisions and deltas from DESIGN.md are in IMPLEMENTATION.md: two summed carry bits per 3-op add (ternary carry is degree 4 after gating), expression carries for 2-op adds, free rotr16/rotr8 byte relabels, inline mu-gated shift identities for rotr12/rotr7, explicit AreBytes for the never-XORed message/OLD_OUT/addr bytes; I/O is keccak.rs's Ecall + x10 register read + per-dword Memw idiom verbatim. Gates: 10/10 canonical 6-round oracle vectors through the syscall (full-width t — the counter-split order is load-bearing); ValueFlow == executor differential; wire audit (every mixing cell written exactly once); e2e test_prove_elfs_blake3 proves+verifies two CHAINED compressions — full bus balance across Ecall/Memw/ByteAlu/AreBytes/IsHalfword on the first run; keccak/ecsm suites unaffected; clippy clean. Known cost: FIXED_TABLE_COUNT 10 -> 11 — every proof now carries a >=4-row BLAKE3 AIR even when unused. This is the PR #871 regression shape at reduced scale; a real-block ABBA is required before merge, and the fallback is gating the table on use. Proof wire format changes (one more sub-proof). count_table_lengths does not yet count blake3 Memw ops (disk-spill sizing only; documented). scripts/gen_blake3_bench.sh generates the dependent-chain bench guest (1 row/compression -> padding-flush at N = 2^k). --- bin/cli/src/main.rs | 9 +- executor/programs/asm/test_blake3.s | 60 + executor/src/tests/blake3_tests.rs | 319 +++++ executor/src/tests/mod.rs | 1 + executor/src/vm/instruction/execution.rs | 171 +++ prover/src/lib.rs | 22 +- prover/src/tables/blake3.rs | 1266 +++++++++++++++++ prover/src/tables/cpu.rs | 11 + prover/src/tables/mod.rs | 1 + prover/src/tables/trace_builder.rs | 246 ++++ prover/src/test_utils.rs | 15 + prover/src/tests/prove_elfs_tests.rs | 49 + scripts/gen_blake3_bench.sh | 99 ++ syscalls/src/syscalls.rs | 28 + thoughts/blake3/blake3-chip/IMPLEMENTATION.md | 95 ++ 15 files changed, 2383 insertions(+), 9 deletions(-) create mode 100644 executor/programs/asm/test_blake3.s create mode 100644 executor/src/tests/blake3_tests.rs create mode 100644 prover/src/tables/blake3.rs create mode 100755 scripts/gen_blake3_bench.sh create mode 100644 thoughts/blake3/blake3-chip/IMPLEMENTATION.md diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index a04e920db..9b3d6cd53 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -412,7 +412,7 @@ fn cmd_execute( // below (the flamegraph path drives execution inside the executor and does // not expose per-log data). `None` means "not counted", so the accel lines // are omitted rather than printed as misleading zeros. - let mut accel_counts: Option<(u64, u64)> = None; + let mut accel_counts: Option<(u64, u64, u64)> = None; let cycle_count = if let Some(ref output_path) = flamegraph.path { // Shared execute+flamegraph path (executor::flamegraph) instead of @@ -479,6 +479,7 @@ fn cmd_execute( let mut cycle_count: u64 = 0; let mut keccak_calls: u64 = 0; + let mut blake3_calls: u64 = 0; let mut ecsm_calls: u64 = 0; // Reused per chunk: `(current_pc, a7)` for logs whose a7 matches an // accelerator syscall number. This is a cheap superset — a non-ECALL @@ -511,6 +512,7 @@ fn cmd_execute( for (pc, a7) in accel_candidates.drain(..) { match accelerator_of(executor.instructions.get(pc), a7) { Some(Accelerator::Keccak) => keccak_calls += 1, + Some(Accelerator::Blake3) => blake3_calls += 1, Some(Accelerator::Ecsm) => ecsm_calls += 1, None => {} } @@ -526,15 +528,16 @@ fn cmd_execute( } if cycles { - accel_counts = Some((keccak_calls, ecsm_calls)); + accel_counts = Some((keccak_calls, blake3_calls, ecsm_calls)); } cycle_count }; if cycles { println!("Cycles: {}", cycle_count); - if let Some((keccak_calls, ecsm_calls)) = accel_counts { + if let Some((keccak_calls, blake3_calls, ecsm_calls)) = accel_counts { println!("Keccak calls: {}", keccak_calls); + println!("Blake3 calls: {}", blake3_calls); println!("Ecsm calls: {}", ecsm_calls); } } diff --git a/executor/programs/asm/test_blake3.s b/executor/programs/asm/test_blake3.s new file mode 100644 index 000000000..d066f4645 --- /dev/null +++ b/executor/programs/asm/test_blake3.s @@ -0,0 +1,60 @@ + .attribute 5, "rv64i2p1_m2p0_zmmul1p0" + .globl main +main: + # 176 bytes on the stack for the BLAKE3 state region (22 x u64): + # h[4 dwords] | m[8] | t[1] | block_len,flags[1] | out[8]. + addi sp, sp, -176 + + # Deterministic non-zero seed over the 14 input dwords: dword[k] = k + 1. + # (t therefore = 13, block_len = 14, flags = 0 — arbitrary but fixed.) + mv t0, sp + li t1, 1 + li t2, 15 +.Linit_loop: + sd t1, 0(t0) + addi t0, t0, 8 + addi t1, t1, 1 + bne t1, t2, .Linit_loop + + # First compression. + # a0 = pointer to the 176-byte region (8-aligned) + # a7 = syscall number (u64::MAX - 2 = -3) + mv a0, sp + li a7, -3 + ecall + + # Chain: copy out (8 dwords at sp+112) over m (8 dwords at sp+32), so the + # second call consumes the first call's output AND its out-region write has + # non-zero previous content. + li t1, 0 +.Lcopy_loop: + slli t2, t1, 3 + addi t3, sp, 112 + add t3, t3, t2 + ld t4, 0(t3) + addi t3, sp, 32 + add t3, t3, t2 + sd t4, 0(t3) + addi t1, t1, 1 + li t2, 8 + bne t1, t2, .Lcopy_loop + + # Second compression. + mv a0, sp + li a7, -3 + ecall + + # Commit the final 64-byte output. + li a0, 1 + addi a1, sp, 112 + li a2, 64 + li a7, 64 + ecall + + # Restore stack and halt. + addi sp, sp, 176 + li a0, 0 + li a7, 93 + ecall +.Lfunc_end0: + .size main, .Lfunc_end0-main diff --git a/executor/src/tests/blake3_tests.rs b/executor/src/tests/blake3_tests.rs new file mode 100644 index 000000000..98bd9af5d --- /dev/null +++ b/executor/src/tests/blake3_tests.rs @@ -0,0 +1,319 @@ +//! Tests for the BLAKE3 6-round compression and its accelerator syscall. +//! +//! Ground truth is the validated oracle (`thoughts/blake3/blake3-oracle/`): +//! the pinned canonical 6-round vectors below were emitted by its harness and +//! checked against the official `blake3` crate at the official test-vector +//! parameters. The `t` values exercise the full 64-bit counter range, pinning +//! the `t_lo → v[12]` / `t_hi → v[13]` split order. + +use crate::vm::instruction::decoding::Instruction; +use crate::vm::instruction::execution::{ + BLAKE3_SYSCALL_NUMBER, ExecutionError, blake3_compress_6round, +}; +use crate::vm::memory::Memory; +use crate::vm::registers::Registers; + +/// One pinned 6-round vector from the validated oracle. +struct Blake3Vector { + h: [u32; 8], + m: [u32; 16], + t: u64, + block_len: u32, + flags: u32, + out: [u32; 16], +} + +/// The 10 canonical 6-round vectors, generated from +/// `thoughts/blake3/blake3-oracle/canonical_6round_vectors.json` (which the +/// oracle harness regenerates and which was validated against the official +/// `blake3` crate). Do not edit by hand. +const CANONICAL_6ROUND_VECTORS: &[Blake3Vector] = &[ + Blake3Vector { + h: [ + 0xd82c07cd, 0x6baa9455, 0x82e2e662, 0x7a024204, 0xe87a1613, 0x81332876, 0x48268673, + 0xc17c6279, + ], + m: [ + 0xe6f4590b, 0x4f65d4d9, 0xbad640fb, 0xaf19922a, 0x19c78df4, 0x6f25e2a2, 0xe9bb17bc, + 0x7a1d5006, 0x42af9fc3, 0x03983ca8, 0xde1b372a, 0xded733e8, 0x9148624f, 0xf7b0b7d2, + 0x72ae2244, 0xeece328b, + ], + t: 0xb4e1357d4a84eb03, + block_len: 42, + flags: 52, + out: [ + 0xced9d1ff, 0xc248eeab, 0xbd109b7f, 0x911b48f6, 0x923d62c0, 0xd804903f, 0x5974223e, + 0xaa4f0c80, 0xad61007f, 0xb50b8ddb, 0xe7372be1, 0x33d3d6c3, 0x42aa284b, 0xc5a25f28, + 0x79ac8370, 0xb75f3915, + ], + }, + Blake3Vector { + h: [ + 0xc386bbc4, 0x414c343c, 0x7311d8a3, 0xa6cecc1b, 0xc9e9c616, 0x18072e8c, 0xd5f4b3b2, + 0x7204e52d, + ], + m: [ + 0xf1fd42a2, 0xe6c3f339, 0x07d4bedc, 0x8a9a021e, 0x3bab6c39, 0x05805975, 0xa46d6753, + 0xdc2574bd, 0xab99254a, 0x4da98f1d, 0xe1ea24c4, 0x815a47c5, 0x08d6af57, 0xcc22af58, + 0x2c4a3698, 0x5fec898f, + ], + t: 0xc74803e31ba16215, + block_len: 50, + flags: 94, + out: [ + 0xf2a972e9, 0x81fdb8ec, 0x40c50ebc, 0x4ba1caf9, 0x9ee9e930, 0x6b1a16b2, 0xe9156f47, + 0xa89fb436, 0xa2f616b3, 0x12874c12, 0x30768035, 0xe01a17d9, 0xbee5c17c, 0xd61c0be0, + 0x3041ff46, 0xdfb91125, + ], + }, + Blake3Vector { + h: [ + 0x0e7a269f, 0x15ba2bdd, 0xd5e34124, 0x4ee207f8, 0x9b1f282e, 0x9b575bd1, 0xf30b94fa, + 0x0706a045, + ], + m: [ + 0x6148a86f, 0x8697bbd0, 0x8f7d9b78, 0x3c729578, 0x061b9030, 0x533c9135, 0x829e07b0, + 0xe4c11ab2, 0xcbf87544, 0xc34c769f, 0x5a91c89b, 0xf63f23d0, 0xc1066932, 0x87c56473, + 0x7d718d73, 0xecc1cb63, + ], + t: 0x7604e4b4e73695c3, + block_len: 58, + flags: 124, + out: [ + 0x5aa6b114, 0xc9d6740c, 0x8738caf4, 0xac5f4b72, 0x9fc6b9de, 0x3f2efb8f, 0x8cb7a912, + 0xf497a285, 0x3d062266, 0x7f22380c, 0xafd468fa, 0x122cba80, 0x446b156d, 0xb239d8c2, + 0xc3eab2cf, 0x775f2f92, + ], + }, + Blake3Vector { + h: [ + 0x8b529b4a, 0x9a9a80fd, 0xd6645fa9, 0x3bfd1d33, 0x79f248b0, 0x268ecc45, 0xa2863a7f, + 0x85ef3430, + ], + m: [ + 0xbdc2ae99, 0x10645d51, 0x97524d6a, 0xdd933160, 0xe0f9e038, 0xebcd1f5e, 0xef829c88, + 0xe0fd67dd, 0x18f2c41c, 0x22cedafb, 0x378c74dc, 0x4d100d8f, 0x95c76ab4, 0x95918694, + 0xe779c470, 0xedcf6109, + ], + t: 0x92d3043afcf249f3, + block_len: 36, + flags: 31, + out: [ + 0xeed92fab, 0x138d9358, 0x915bfe3c, 0x13718b01, 0xb506e277, 0xbe4007cd, 0x35847e06, + 0xce1c6896, 0x52fa01b5, 0x4aa26af8, 0xb1078a61, 0x2c517aed, 0xa08867a0, 0xea6ecfea, + 0x6d33d3b0, 0xdc293166, + ], + }, + Blake3Vector { + h: [ + 0x3c6da5d7, 0x656412a9, 0x27ac435a, 0x11072231, 0xeaff1a09, 0xc3e1b258, 0x8963dc6e, + 0x1b2ed40e, + ], + m: [ + 0xed6f0b09, 0xce80c4b0, 0xccea2645, 0x3184ff27, 0x4f5253a0, 0xe14b0190, 0x9b191bf4, + 0xabf4a07c, 0x81862fc9, 0x2d83a823, 0x793d0e45, 0x4cdce7a6, 0xe8abb93f, 0xe1df8af9, + 0x8224b122, 0x69f85e31, + ], + t: 0x49c7b59b995253fd, + block_len: 57, + flags: 41, + out: [ + 0xca00bda3, 0x84239a3a, 0xe7c88e6d, 0x33a8a3d6, 0x09dcd1ce, 0xa1b10212, 0xf48e1156, + 0x8f039915, 0x8a055eaa, 0xff5b11d5, 0xb725085b, 0x2e1ab267, 0x6ae7323d, 0xb2ff6fa8, + 0x7102c8a1, 0x7561eb37, + ], + }, + Blake3Vector { + h: [ + 0x9f767c45, 0xbde5c099, 0xf17fd374, 0xa6233255, 0xe6a16a3b, 0x1cfb10f6, 0x3f1f65a8, + 0x8b33e968, + ], + m: [ + 0x92edcf45, 0x377b9aa2, 0x478c281d, 0xc4069545, 0xcc11d357, 0x9e115e4b, 0x206f5c66, + 0xdf1461aa, 0xfb7ff337, 0xdf561d80, 0x4a0fe75d, 0xf6236bf2, 0x346c6e2b, 0xb0cde917, + 0xe4cc4132, 0x4c7d6df0, + ], + t: 0x6a3753915c76f18a, + block_len: 18, + flags: 67, + out: [ + 0x14a9f66f, 0x101bdfe8, 0x9b0a50dd, 0xee4bb45b, 0x7a914502, 0x77b3486b, 0x59bfc114, + 0xa1ad2afd, 0xc194dde6, 0x894ec54d, 0xad36c805, 0x9018f3f5, 0x165af5d8, 0x3e85b598, + 0x78e76653, 0xbb7a485d, + ], + }, + Blake3Vector { + h: [ + 0xd26b9496, 0x42f9a039, 0x001d9a88, 0x5f877031, 0xc527e279, 0x45cf8aa4, 0xcd4a5557, + 0xae9af169, + ], + m: [ + 0xaf895f5b, 0xd822e2f9, 0x17d7ab26, 0xccdf540b, 0xce06294d, 0x4a8b0188, 0xf38d2e64, + 0x5c41d5c5, 0xe8d5b9e3, 0x5c832a51, 0x9a0c1b76, 0x4de8344e, 0x96d2f9e0, 0x8677a5f2, + 0xa9a967c1, 0x323bbeaf, + ], + t: 0x390567c27bd6aa42, + block_len: 26, + flags: 3, + out: [ + 0x32a6ff70, 0xc30560bc, 0xd1c777c8, 0xf1871821, 0x7207ab54, 0x9f5b83c7, 0xb6561c5d, + 0x991e738f, 0xb38b62b9, 0x0ef6d156, 0x994becb1, 0x09a85d0e, 0x32221741, 0xada3cc5f, + 0x5b654ed6, 0x2a7a62b2, + ], + }, + Blake3Vector { + h: [ + 0x269e0d37, 0xa6a3a450, 0x892f902b, 0x81e74ef5, 0x099950d8, 0x6f03675a, 0x11e20b8f, + 0x6cad4a26, + ], + m: [ + 0xf29d0da9, 0x658cda14, 0xf9ebdacc, 0xdbc496cb, 0x4a23d596, 0x2e44158b, 0xa38fd547, + 0x5f557203, 0x34b9b5df, 0x506bf2ef, 0x7403e430, 0x4cbd87ad, 0xcb5c7427, 0x3e7d1bfb, + 0x930d6eaf, 0x86734721, + ], + t: 0x12bd4acefaecbd38, + block_len: 53, + flags: 42, + out: [ + 0xa632ad45, 0x12ce41f4, 0xd21b2cbd, 0x76795c62, 0x6bec36c1, 0xdafafcde, 0x53ca87b7, + 0x92e8465b, 0x7b424f5d, 0xe1e6ad7f, 0x753ba387, 0xccc50824, 0x69aedf6d, 0xbbbbf253, + 0x78d04883, 0xf3f33689, + ], + }, + Blake3Vector { + h: [ + 0x3a096533, 0xf658f7a7, 0x205738d1, 0xb46ee1da, 0x15ceb3a1, 0x359b1548, 0xa4517d6c, + 0x7589ca4a, + ], + m: [ + 0x74007cb4, 0xd49d0ac1, 0x16edc5d4, 0x685ca8af, 0x4223aa56, 0x10269470, 0x60908405, + 0xa92d04a3, 0x56a3e957, 0xb0f91306, 0xe6c08269, 0xf2306d4a, 0x31a06a7c, 0x9436d6f6, + 0xe18692e2, 0xe0c99f3e, + ], + t: 0x329911da9fbd8735, + block_len: 19, + flags: 91, + out: [ + 0x913b2ae1, 0xc7f73082, 0x45e1c023, 0x6f1f3f82, 0x20aee6f5, 0xdaf21d94, 0xf2c1e4af, + 0xd4f7d4ac, 0x44a45f87, 0xf4c40ce5, 0x613e9b94, 0x08ce53de, 0x4ff07aa4, 0x456bf2e2, + 0x2066ea7f, 0x3c5a654b, + ], + }, + Blake3Vector { + h: [ + 0x5f915ef0, 0x237751aa, 0x01a5ba50, 0x80b65386, 0x14b044d7, 0x61076dc3, 0xb99de255, + 0x283b73a6, + ], + m: [ + 0x3cee5e2c, 0x1c670ea9, 0x972651da, 0x4a8aa593, 0xac9abb0c, 0x35bb5c11, 0x47fbb3b4, + 0xcf3c17e5, 0xe2eb17c8, 0xe11e99fb, 0x7de0d208, 0x0602fe0c, 0x98cae043, 0x9425b3e2, + 0x33fb4b4f, 0x15607df9, + ], + t: 0xeaeb999b8a2e547e, + block_len: 64, + flags: 21, + out: [ + 0xf5ee9114, 0x856cabb8, 0x29be2cf1, 0x603be91c, 0x94a7dd0e, 0x28fc3e27, 0xb64e2cc8, + 0x2d2c67ff, 0x69fac1ba, 0x0c949090, 0xd68de435, 0xce91a527, 0xe80c1815, 0x6d44efe6, + 0x87c7b175, 0xd18a8b94, + ], + }, +]; + +#[test] +fn test_blake3_6round_canonical_vectors() { + for (i, v) in CANONICAL_6ROUND_VECTORS.iter().enumerate() { + let out = blake3_compress_6round(&v.h, &v.m, v.t, v.block_len, v.flags); + assert_eq!(out, v.out, "canonical 6-round vector {i} mismatch"); + } +} + +#[test] +fn test_blake3_syscall_matches_vectors() { + for (i, v) in CANONICAL_6ROUND_VECTORS.iter().enumerate() { + let mut pc = 0; + let mut registers = Registers::default(); + let mut memory = Memory::default(); + let addr = 0x1000u64; + + // Lay out the 176-byte state region: h | m | t | (block_len, flags) | out. + let mut words = [0u32; 28]; + words[0..8].copy_from_slice(&v.h); + words[8..24].copy_from_slice(&v.m); + words[24] = v.t as u32; + words[25] = (v.t >> 32) as u32; + words[26] = v.block_len; + words[27] = v.flags; + for k in 0..14 { + let dw = (words[2 * k] as u64) | ((words[2 * k + 1] as u64) << 32); + memory.store_doubleword(addr + (k as u64) * 8, dw).unwrap(); + } + // Pre-fill the out region so the test catches a partial write. + for k in 14..22 { + memory + .store_doubleword(addr + (k as u64) * 8, 0xDEAD_BEEF_DEAD_BEEFu64) + .unwrap(); + } + + registers.write(17, BLAKE3_SYSCALL_NUMBER).unwrap(); + registers.write(10, addr).unwrap(); + Instruction::EcallEbreak + .run(&mut pc, &mut registers, &mut memory) + .unwrap(); + + let mut got = [0u32; 16]; + for k in 0..8 { + let dw = memory + .load_doubleword(addr + ((14 + k) as u64) * 8) + .unwrap(); + got[2 * k] = dw as u32; + got[2 * k + 1] = (dw >> 32) as u32; + } + assert_eq!(got, v.out, "syscall output mismatch on vector {i}"); + + // The 112 input bytes must be untouched. + for k in 0..14 { + let dw = memory.load_doubleword(addr + (k as u64) * 8).unwrap(); + let expected = (words[2 * k] as u64) | ((words[2 * k + 1] as u64) << 32); + assert_eq!(dw, expected, "input dword {k} clobbered on vector {i}"); + } + } +} + +#[test] +fn test_blake3_syscall_rejects_unaligned_state_addr() { + let mut pc = 0; + let mut registers = Registers::default(); + let mut memory = Memory::default(); + + registers.write(17, BLAKE3_SYSCALL_NUMBER).unwrap(); + registers.write(10, 0x1004).unwrap(); + + let err = Instruction::EcallEbreak + .run(&mut pc, &mut registers, &mut memory) + .unwrap_err(); + assert!(matches!( + err, + ExecutionError::UnalignedBlake3StateAddress(0x1004) + )); +} + +#[test] +fn test_blake3_syscall_rejects_overflowing_state_range() { + let mut pc = 0; + let mut registers = Registers::default(); + let mut memory = Memory::default(); + + registers.write(17, BLAKE3_SYSCALL_NUMBER).unwrap(); + // 22 dwords = 176 bytes; addr + 175 must not overflow. u64::MAX - 167 is + // 8-aligned and the last byte lands at u64::MAX + 8 → overflow. + registers.write(10, u64::MAX - 167).unwrap(); + + let err = Instruction::EcallEbreak + .run(&mut pc, &mut registers, &mut memory) + .unwrap_err(); + assert!(matches!( + err, + ExecutionError::Blake3StateAddressOverflow(addr) if addr == u64::MAX - 167 + )); +} diff --git a/executor/src/tests/mod.rs b/executor/src/tests/mod.rs index 456607433..90bb1331c 100644 --- a/executor/src/tests/mod.rs +++ b/executor/src/tests/mod.rs @@ -1,3 +1,4 @@ +pub mod blake3_tests; pub mod ecsm_tests; pub mod flamegraph_tests; pub mod keccak_tests; diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index c92c0ab88..7e934c585 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -12,6 +12,8 @@ pub enum SyscallNumbers { KeccakPermute = 0, Print = 1, Panic = 2, + // Placeholder discriminant. The actual syscall value is BLAKE3_SYSCALL_NUMBER. + Blake3Compress = 3, Commit = 64, Halt = 93, // Placeholder discriminant. The actual syscall value is ECSM_SYSCALL_NUMBER. @@ -24,6 +26,30 @@ pub enum SyscallNumbers { pub const KECCAK_SYSCALL_NUMBER: u64 = u64::MAX - 1; const KECCAK_STATE_BYTES: u64 = 25 * 8; +/// Syscall number for the BLAKE3 6-round compression accelerator +/// (u64::MAX - 2 = 0xFFFF_FFFF_FFFF_FFFD). +/// +/// This is the **6-round internal variant** of the BLAKE3 compression function +/// (`thoughts/blake3/blake3-chip/DESIGN.md`), intended for in-house Merkle / +/// Fiat–Shamir use — it is NOT standard 7-round BLAKE3 and its security rests +/// on the named 6-round assumption recorded in the design. +/// +/// ABI: `x10` = 8-byte-aligned pointer to a 176-byte state region laid out as +/// consecutive little-endian dwords at `addr + 8k`: +/// +/// | dword k | contents | +/// |---------|--------------------------------------------| +/// | 0..=3 | `h[0..8]` chaining value (2 u32 words/dword) | +/// | 4..=11 | `m[0..16]` message block | +/// | 12 | `t` counter (`t_lo = low u32 → v[12]`, `t_hi = high u32 → v[13]`) | +/// | 13 | `block_len` (low u32) \| `flags` (high u32) | +/// | 14..=21 | `out[0..16]` — written by the syscall | +pub const BLAKE3_SYSCALL_NUMBER: u64 = u64::MAX - 2; +/// Bytes of the BLAKE3 state region: 112 input + 64 output. +const BLAKE3_STATE_BYTES: u64 = 22 * 8; +/// Dword offset of `out[0..16]` inside the BLAKE3 state region. +const BLAKE3_OUT_DWORDS: u64 = 14; + /// Syscall number for the ECSM (elliptic-curve scalar multiply) accelerator. /// /// The spec uses ECALL number `-11`; interpreted as an unsigned 64-bit value that is @@ -44,6 +70,7 @@ impl TryFrom for SyscallNumbers { 64 => Ok(SyscallNumbers::Commit), 93 => Ok(SyscallNumbers::Halt), v if v == KECCAK_SYSCALL_NUMBER => Ok(SyscallNumbers::KeccakPermute), + v if v == BLAKE3_SYSCALL_NUMBER => Ok(SyscallNumbers::Blake3Compress), v if v == ECSM_SYSCALL_NUMBER => Ok(SyscallNumbers::Ecsm), _ => Err(()), } @@ -54,6 +81,7 @@ impl TryFrom for SyscallNumbers { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Accelerator { Keccak, + Blake3, Ecsm, } @@ -64,6 +92,7 @@ impl SyscallNumbers { pub fn accelerator(self) -> Option { match self { SyscallNumbers::KeccakPermute => Some(Accelerator::Keccak), + SyscallNumbers::Blake3Compress => Some(Accelerator::Blake3), SyscallNumbers::Ecsm => Some(Accelerator::Ecsm), SyscallNumbers::Print | SyscallNumbers::Panic @@ -421,6 +450,41 @@ impl Instruction { } src2_val = state_addr; } + SyscallNumbers::Blake3Compress => { + // BLAKE3 6-round compression on a 176-byte region at the + // address in x10 (layout: see BLAKE3_SYSCALL_NUMBER docs). + let state_addr = registers.read(10)?; + if !state_addr.is_multiple_of(8) { + return Err(ExecutionError::UnalignedBlake3StateAddress(state_addr)); + } + state_addr + .checked_add(BLAKE3_STATE_BYTES - 1) + .ok_or(ExecutionError::Blake3StateAddressOverflow(state_addr))?; + + // Input: 14 dwords = h[8] | m[16] | t | (block_len, flags), + // each dword two little-endian u32 words. + let mut words = [0u32; 28]; + for k in 0..14 { + let dw = memory.load_doubleword(state_addr + (k as u64) * 8)?; + words[2 * k] = dw as u32; + words[2 * k + 1] = (dw >> 32) as u32; + } + let h: [u32; 8] = words[0..8].try_into().unwrap(); + let m: [u32; 16] = words[8..24].try_into().unwrap(); + let t = (words[24] as u64) | ((words[25] as u64) << 32); + let block_len = words[26]; + let flags = words[27]; + + let out = blake3_compress_6round(&h, &m, t, block_len, flags); + for k in 0..8 { + let dw = (out[2 * k] as u64) | ((out[2 * k + 1] as u64) << 32); + memory.store_doubleword( + state_addr + (BLAKE3_OUT_DWORDS + k as u64) * 8, + dw, + )?; + } + src2_val = state_addr; + } SyscallNumbers::Ecsm => { // ECSM(-11): k×G on secp256k1. // x10 = addr to write xR, x11 = addr of xG, x12 = addr of k. @@ -630,6 +694,10 @@ pub enum ExecutionError { UnalignedKeccakStateAddress(u64), #[error("Keccak state address range overflows: {0:#018x}")] KeccakStateAddressOverflow(u64), + #[error("Unaligned BLAKE3 state address: {0:#018x}")] + UnalignedBlake3StateAddress(u64), + #[error("BLAKE3 state address range overflows: {0:#018x}")] + Blake3StateAddressOverflow(u64), #[error("ECSM address range overflows the lower 32-bit limb")] EcsmAddressOverflow, #[error("ECSM xG and k operand ranges overlap")] @@ -719,3 +787,106 @@ pub fn keccak_f1600(state: &mut [u64; 25]) { state[0] ^= rc; } } + +// ============================================================================= +// BLAKE3 6-round compression (internal variant) +// ============================================================================= +// +// A Rust port of the validated oracle `thoughts/blake3/blake3-oracle/blake3_ref.py` +// with `rounds = 6` fixed. This is the **6-round internal variant** — NOT +// standard BLAKE3 (7 rounds); its security rests on the named 6-round +// assumption recorded in `thoughts/blake3/blake3-chip/DESIGN.md`. Differentially +// tested against the oracle's canonical 6-round vectors (pinned in +// `thoughts/blake3/blake3-oracle/canonical_6round_vectors.json`, themselves +// validated against the official `blake3` crate). + +/// The BLAKE3 IV (identical to SHA-256's initial state). `IV[0..4]` seeds +/// `v[8..12]` of the compression working state. +pub const BLAKE3_IV: [u32; 8] = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +]; + +/// The BLAKE3 message-schedule permutation, applied between rounds +/// (`m'[i] = m[MSG_PERMUTATION[i]]`). +pub const BLAKE3_MSG_PERMUTATION: [usize; 16] = + [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]; + +/// Rounds of the internal variant. 6, per the design; standard BLAKE3 is 7. +pub const BLAKE3_ROUNDS: usize = 6; + +/// The BLAKE3 quarter-round G (spec §2.1). +#[inline] +fn blake3_g(v: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, mx: u32, my: u32) { + v[a] = v[a].wrapping_add(v[b]).wrapping_add(mx); + v[d] = (v[d] ^ v[a]).rotate_right(16); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(12); + v[a] = v[a].wrapping_add(v[b]).wrapping_add(my); + v[d] = (v[d] ^ v[a]).rotate_right(8); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(7); +} + +/// The BLAKE3 compression function `f` at 6 rounds (spec §2.2, oracle §2.4). +/// +/// State init: `v[0..8] = h`, `v[8..12] = IV[0..4]`, `v[12] = t as u32`, +/// `v[13] = (t >> 32) as u32`, `v[14] = block_len`, `v[15] = flags`. Six +/// rounds of 8 G-calls (4 columns then 4 diagonals), permuting the message +/// schedule between rounds (`r < rounds - 1`, i.e. 5 permutes — the trailing +/// permute is never consumed). Feed-forward: `out[i] = v[i] ^ v[i+8]`, +/// `out[i+8] = v[i+8] ^ h[i]`. The truncated chaining value is `out[0..8]`. +pub fn blake3_compress_6round( + h: &[u32; 8], + m: &[u32; 16], + t: u64, + block_len: u32, + flags: u32, +) -> [u32; 16] { + let mut v: [u32; 16] = [ + h[0], + h[1], + h[2], + h[3], + h[4], + h[5], + h[6], + h[7], + BLAKE3_IV[0], + BLAKE3_IV[1], + BLAKE3_IV[2], + BLAKE3_IV[3], + t as u32, + (t >> 32) as u32, + block_len, + flags, + ]; + + let mut m = *m; + for r in 0..BLAKE3_ROUNDS { + // Mix the columns. + blake3_g(&mut v, 0, 4, 8, 12, m[0], m[1]); + blake3_g(&mut v, 1, 5, 9, 13, m[2], m[3]); + blake3_g(&mut v, 2, 6, 10, 14, m[4], m[5]); + blake3_g(&mut v, 3, 7, 11, 15, m[6], m[7]); + // Mix the diagonals. + blake3_g(&mut v, 0, 5, 10, 15, m[8], m[9]); + blake3_g(&mut v, 1, 6, 11, 12, m[10], m[11]); + blake3_g(&mut v, 2, 7, 8, 13, m[12], m[13]); + blake3_g(&mut v, 3, 4, 9, 14, m[14], m[15]); + // Permute between rounds; the permute after the last round is never + // consumed (oracle: `r < rounds - 1`). + if r < BLAKE3_ROUNDS - 1 { + let prev = m; + for (i, &p) in BLAKE3_MSG_PERMUTATION.iter().enumerate() { + m[i] = prev[p]; + } + } + } + + let mut out = [0u32; 16]; + for i in 0..8 { + out[i] = v[i] ^ v[i + 8]; + out[i + 8] = v[i + 8] ^ h[i]; + } + out +} diff --git a/prover/src/lib.rs b/prover/src/lib.rs index a8e89f989..7f3731fde 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -51,10 +51,10 @@ use crate::tables::trace_builder::Traces; use crate::tables::trace_builder::count_table_lengths; use crate::tables::types::BusId; use crate::test_utils::{ - E, F, VmAir, create_bitwise_air, create_branch_air, create_bytewise_air, create_commit_air, - create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, create_ecdas_air, - create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, create_keccak_rc_air, - create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, + E, F, VmAir, create_bitwise_air, create_blake3_air, create_branch_air, create_bytewise_air, + create_commit_air, create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, + create_ecdas_air, create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, + create_keccak_rc_air, create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, create_memw_aligned_air, create_memw_register_air, create_mul_air, create_page_air, create_register_air, create_shift_air, create_store_air, }; @@ -82,8 +82,13 @@ pub struct RuntimePageRange { /// Number of tables that always contribute exactly one sub-proof, regardless /// of `TableCounts`: bitwise, decode, halt, commit, keccak, keccak_rnd, -/// keccak_rc, register, ecsm, ecdas. -pub const FIXED_TABLE_COUNT: usize = 10; +/// keccak_rc, blake3, register, ecsm, ecdas. +/// +/// ⚠ Every always-on table costs every proof a near-empty AIR even when the +/// workload never touches it (the EC-campaign lesson, PR #871). BLAKE3 adds +/// one (min 4 rows × ~3.2k cols); its real-workload cost must be ABBA-checked +/// before this merges. +pub const FIXED_TABLE_COUNT: usize = 11; /// Number of chunks for each split table. /// The verifier needs this to reconstruct matching AIRs. @@ -515,6 +520,7 @@ pub(crate) struct VmAirs { pub keccak: VmAir, pub keccak_rnd: VmAir, pub keccak_rc: VmAir, + pub blake3: VmAir, pub ecsm: VmAir, pub ecdas: VmAir, pub register: VmAir, @@ -540,6 +546,7 @@ impl VmAirs { (self.keccak.as_ref(), &mut traces.keccak, &()), (self.keccak_rnd.as_ref(), &mut traces.keccak_rnd, &()), (self.keccak_rc.as_ref(), &mut traces.keccak_rc, &()), + (self.blake3.as_ref(), &mut traces.blake3, &()), (self.ecsm.as_ref(), &mut traces.ecsm, &()), (self.ecdas.as_ref(), &mut traces.ecdas, &()), (self.register.as_ref(), &mut traces.register, &()), @@ -614,6 +621,7 @@ impl VmAirs { self.keccak.as_ref(), self.keccak_rnd.as_ref(), self.keccak_rc.as_ref(), + self.blake3.as_ref(), self.ecsm.as_ref(), self.ecdas.as_ref(), self.register.as_ref(), @@ -767,6 +775,7 @@ impl VmAirs { let commit: VmAir = Box::new(create_commit_air(proof_options)); let keccak: VmAir = Box::new(create_keccak_air(proof_options)); let keccak_rnd: VmAir = Box::new(create_keccak_rnd_air(proof_options)); + let blake3: VmAir = Box::new(create_blake3_air(proof_options)); let keccak_rc: VmAir = Box::new(create_keccak_rc_air(proof_options).with_preprocessed( tables::keccak_rc::preprocessed_commitment(proof_options), tables::keccak_rc::NUM_PRECOMPUTED_COLS, @@ -877,6 +886,7 @@ impl VmAirs { keccak, keccak_rnd, keccak_rc, + blake3, ecsm, ecdas, register, diff --git a/prover/src/tables/blake3.rs b/prover/src/tables/blake3.rs new file mode 100644 index 000000000..4c08d5a5c --- /dev/null +++ b/prover/src/tables/blake3.rs @@ -0,0 +1,1266 @@ +//! BLAKE3 6-round compression accelerator chip (syscall variant). +//! +//! One row per compression call, fully unrolled (Layout B of +//! `thoughts/blake3/blake3-chip/DESIGN.md`): all 6 rounds × 8 G-functions are +//! laid out in SSA form across the row, so the message schedule is a +//! compile-time permutation of the 16 committed message words and there is no +//! state/message handoff between rows. +//! +//! I/O follows the KECCAK core idiom (`keccak.rs`): an `Ecall` receiver binds +//! (timestamp, syscall#), a `Memw` register read binds the x10 state pointer, +//! and per-dword `Memw` ops read the 112 input bytes / write the 64 output +//! bytes. The 176-byte state region layout is documented on +//! [`executor::vm::instruction::execution::BLAKE3_SYSCALL_NUMBER`]. +//! +//! ## The single-dataflow rule +//! +//! The compression dataflow is written ONCE, in [`run_flow`], and interpreted +//! twice: [`WireFlow`] (columns — drives the constraints and bus senders) and +//! [`ValueFlow`] (u32 witness — drives trace filling and the BITWISE +//! multiplicity collection in `trace_builder.rs`). The two cannot diverge on +//! wiring, only on interpretation, which the e2e bus-balance gate checks. +//! +//! ## Soundness ledger (DESIGN.md §7, adapted to the syscall variant) +//! +//! 1. Every eval constraint is μ-gated; padding rows are all-zero (except the +//! keccak-style PTR pad) with μ=0. +//! 2. 3-op adds use TWO summed committed carry bits + the explicit sum +//! identity (a ternary carry would be degree 4 after gating). +//! 3. 2-op adds use the `emit_add_pair`-style expression carry (no committed +//! cell) with μ-gated booleanity; the output's bytes are range-checked by +//! the downstream XOR lookup that consumes them. +//! 4. Every add/shift output feeds a downstream `ByteAlu` XOR — that lookup is +//! its only byte range check. The last-round outputs are consumed by the +//! feed-forward XORs, closing the chain. +//! 5. The message words `m` are never XORed, so their 64 bytes get explicit +//! `AreBytes` sends. Same for the 64 `OLD_OUT` bytes (the previous memory +//! content of the out region, which appear only on the Memw bus) and the 8 +//! address bytes (aliasing — see keccak.rs's addr comment). +//! 6. rotr16/rotr8 are free byte relabels `[b2,b3,b0,b1]` / `[b1,b2,b3,b0]`. +//! 7. rotr12/rotr7 are inline μ-gated shift identities with `AreBytes` on all +//! four shift halfwords (`SLL_lo/SLLC_lo/SLL_hi/SLLC_hi`); soundness needs +//! the tight bound on the `SLL` pair (2^16 invertible mod p — the audited +//! Euclidean-division argument). +//! 8. The message schedule is `permute^r` wired from the ORIGINAL M columns. +//! 9. All identities stay < 2^35 ≪ p (non-overflow side conditions), given +//! byte-range operands and boolean carries. +//! 10. (Internal-bus binding — N/A here: the syscall variant has no `Blake3` +//! bus; a row's inputs and outputs are tied by being the same row.) +//! +//! ⚠ This chip implements the **6-round internal variant** — NOT standard +//! 7-round BLAKE3. Its collision resistance is a named assumption +//! (DESIGN.md "If this is picked up again"). + +use executor::vm::instruction::execution::{ + BLAKE3_IV, BLAKE3_MSG_PERMUTATION, BLAKE3_ROUNDS, BLAKE3_SYSCALL_NUMBER, +}; +use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; +use stark::trace::TraceTable; + +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; + +use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; +use crate::constraints::templates::{AddOperand, INV_SHIFT_32}; + +/// G-instances per compression: 8 per round × 6 rounds. +pub const NUM_G: usize = BLAKE3_ROUNDS * 8; + +/// Dwords in the state region: 14 input (h|m|t|len_flags) + 8 output. +pub const STATE_DWORDS: usize = 22; +/// Input dwords (read-only). +pub const IN_DWORDS: usize = 14; + +/// The (a, b, c, d) state indices of the 8 G-calls of one round: +/// 4 column mixes then 4 diagonal mixes (BLAKE3 spec §2.1). +const G_INDICES: [(usize, usize, usize, usize); 8] = [ + (0, 4, 8, 12), + (1, 5, 9, 13), + (2, 6, 10, 14), + (3, 7, 11, 15), + (0, 5, 10, 15), + (1, 6, 11, 12), + (2, 7, 8, 13), + (3, 4, 9, 14), +]; + +/// Shift amounts of the two non-free rotations, as `rotl` inner shifts: +/// rotr12 = rotl20 = rotl16∘rotl4 (r=4); rotr7 = rotl25 = rotl16∘rotl9 (r=9). +const ROT_SHIFT_R: [u32; 2] = [4, 9]; + +// ========================================================================= +// Column indices +// ========================================================================= + +pub mod cols { + use super::NUM_G; + + pub const TIMESTAMP_0: usize = 0; + pub const TIMESTAMP_1: usize = 1; + + /// State address as 8 bytes (DWordBL). + pub const ADDR: usize = 2; + + /// Per-dword pointers [22][4] halfwords (DWordHL), ptr[k] = addr + 8k. + pub const PTR: usize = ADDR + 8; // 10 + + /// Input bytes: h[32] | m[64] | t_lo[4] | t_hi[4] | block_len[4] | flags[4]. + pub const IN: usize = PTR + super::STATE_DWORDS * 4; // 98 + + /// 48 G-blocks × 60 cells (56 bytes + 4 carry bits) — see `g` accessors. + pub const G: usize = IN + 112; // 210 + pub const G_SIZE: usize = 60; + + /// Feed-forward output bytes out[0..16] (64 bytes). + pub const OUT: usize = G + NUM_G * G_SIZE; // 3090 + + /// Previous memory content of the out region (64 bytes). Appears only in + /// the Memw write ops' `old` field; range-checked by AreBytes. + pub const OLD_OUT: usize = OUT + 64; // 3154 + + /// Multiplicity / gate flag. + pub const MU: usize = OLD_OUT + 64; // 3218 + + pub const NUM_COLUMNS: usize = MU + 1; // 3219 + + // ------------------------------------------------------------------------- + // Index helpers + // ------------------------------------------------------------------------- + + #[inline] + pub const fn addr(byte: usize) -> usize { + ADDR + byte + } + + /// ptr[k][hw] — halfword hw of the pointer to dword k. + #[inline] + pub const fn ptr(k: usize, hw: usize) -> usize { + PTR + k * 4 + hw + } + + /// Input word i (0..28: h[0..8], m[8..24], t_lo=24, t_hi=25, len=26, flags=27), + /// byte b. + #[inline] + pub const fn in_word(i: usize, b: usize) -> usize { + IN + i * 4 + b + } + + /// Base column of G-block g. + #[inline] + pub const fn g_base(g: usize) -> usize { + G + g * G_SIZE + } + + // Offsets inside one G block (56 byte cells + 4 carry bits = 60): + /// add3 #1 output word (4 bytes). + pub const G_A1: usize = 0; + /// add3 #1 carry bits c1, c2. + pub const G_A1_C: usize = 4; + /// X1 = vd ^ A1 (4 bytes). + pub const G_X1: usize = 6; + /// add2 #1 output word (4 bytes). + pub const G_C1: usize = 10; + /// X2 = vb ^ C1 (4 bytes). + pub const G_X2: usize = 14; + /// rotr12 block: SLL_lo(2) SLLC_lo(2) SLL_hi(2) SLLC_hi(2) Y(4). + pub const G_R1: usize = 18; + /// add3 #2 output word (4 bytes). + pub const G_A2: usize = 30; + /// add3 #2 carry bits. + pub const G_A2_C: usize = 34; + /// X3 = vd ^ A2 (4 bytes). + pub const G_X3: usize = 36; + /// add2 #2 output word (4 bytes). + pub const G_C2: usize = 40; + /// X4 = B1 ^ C2 (4 bytes). + pub const G_X4: usize = 44; + /// rotr7 block: same layout as G_R1. + pub const G_R2: usize = 48; + + /// Feed-forward output word i (0..16), byte b. + #[inline] + pub const fn out_word(i: usize, b: usize) -> usize { + OUT + i * 4 + b + } + + /// Previous-content byte b (0..64) of the out region. + #[inline] + pub const fn old_out(b: usize) -> usize { + OLD_OUT + b + } +} + +// ========================================================================= +// The single dataflow, interpreted twice +// ========================================================================= + +/// The BLAKE3 compression dataflow, abstracted over its word representation. +/// +/// [`run_flow`] is the only place the G-function wiring, message schedule and +/// feed-forward exist; implementors interpret the primitive ops either as +/// column wiring ([`WireFlow`]) or as u32 witness computation ([`ValueFlow`]). +pub(crate) trait Blake3Flow { + type Word: Copy; + + /// h[i] input word. + fn input_h(&mut self, i: usize) -> Self::Word; + /// v[12..16] init words: t_lo, t_hi, block_len, flags. + fn input_v12(&mut self, j: usize) -> Self::Word; + /// IV[i] constant (v[8..12]). + fn iv_const(&mut self, i: usize) -> Self::Word; + + /// 3-operand add `s = a + b + m[m_idx] mod 2^32` (half 0/1 = which add3 of G g). + fn add3( + &mut self, + g: usize, + half: usize, + a: Self::Word, + b: Self::Word, + m_idx: usize, + ) -> Self::Word; + /// 2-operand add `s = a + b mod 2^32`. + fn add2(&mut self, g: usize, half: usize, a: Self::Word, b: Self::Word) -> Self::Word; + /// XOR (slot 0..4 = X1..X4 of G g). Operand order is part of the wire format. + fn xor(&mut self, g: usize, slot: usize, a: Self::Word, b: Self::Word) -> Self::Word; + /// rotr16: free byte relabel [b2,b3,b0,b1]. + fn rotr16(&mut self, w: Self::Word) -> Self::Word; + /// rotr8: free byte relabel [b1,b2,b3,b0]. + fn rotr8(&mut self, w: Self::Word) -> Self::Word; + /// rotr12 (half=0) / rotr7 (half=1) via the inline shift identity. + fn rot_shift(&mut self, g: usize, half: usize, w: Self::Word) -> Self::Word; + /// Feed-forward XOR pair: out[i] = v[i] ^ v[i+8], out[i+8] = v[i+8] ^ h[i]. + fn feed_forward(&mut self, i: usize, vi: Self::Word, vi8: Self::Word, hi: Self::Word); +} + +/// Drive the full 6-round compression through `f`. The message schedule is +/// tracked as indices into the ORIGINAL m (permute^r composition), so both +/// interpretations reference original message words — never copies. +pub(crate) fn run_flow(f: &mut F) { + let h: [F::Word; 8] = core::array::from_fn(|i| f.input_h(i)); + let mut v: [F::Word; 16] = core::array::from_fn(|i| { + if i < 8 { + h[i] + } else if i < 12 { + f.iv_const(i - 8) + } else { + f.input_v12(i - 12) + } + }); + + // sched[i] = index into the original m of the word consumed at position i + // this round. permute: m'[i] = m[P[i]] ⇒ sched'[i] = sched[P[i]]. + let mut sched: [usize; 16] = core::array::from_fn(|i| i); + + for r in 0..BLAKE3_ROUNDS { + for (j, &(ia, ib, ic, id)) in G_INDICES.iter().enumerate() { + let g = r * 8 + j; + let (va, vb, vc, vd) = (v[ia], v[ib], v[ic], v[id]); + let mx = sched[2 * j]; + let my = sched[2 * j + 1]; + + let a1 = f.add3(g, 0, va, vb, mx); + let x1 = f.xor(g, 0, vd, a1); + let vd1 = f.rotr16(x1); + let c1 = f.add2(g, 0, vc, vd1); + let x2 = f.xor(g, 1, vb, c1); + let b1 = f.rot_shift(g, 0, x2); // rotr12 + let a2 = f.add3(g, 1, a1, b1, my); + let x3 = f.xor(g, 2, vd1, a2); + let vd2 = f.rotr8(x3); + let c2 = f.add2(g, 1, c1, vd2); + let x4 = f.xor(g, 3, b1, c2); + let b2 = f.rot_shift(g, 1, x4); // rotr7 + + v[ia] = a2; + v[ib] = b2; + v[ic] = c2; + v[id] = vd2; + } + if r < BLAKE3_ROUNDS - 1 { + let prev = sched; + for (i, &p) in BLAKE3_MSG_PERMUTATION.iter().enumerate() { + sched[i] = prev[p]; + } + } + } + + for i in 0..8 { + f.feed_forward(i, v[i], v[i + 8], h[i]); + } +} + +// ========================================================================= +// Wire interpretation (columns) +// ========================================================================= + +/// A 32-bit word as wiring: four byte columns (LSB first) or a constant. +/// Constants only ever appear as the IV `v[c]` operands of round-0 add2s. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum WordRef { + Cols([usize; 4]), + Const(u32), +} + +impl WordRef { + fn byte(self, b: usize) -> ByteRef { + match self { + WordRef::Cols(c) => ByteRef::Col(c[b]), + WordRef::Const(w) => ByteRef::Const(((w >> (8 * b)) & 0xFF) as u8), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ByteRef { + Col(usize), + Const(u8), +} + +/// One recorded 3-op add: operands (a, b, m columns), output columns, carries. +pub(crate) struct Add3Wire { + pub a: WordRef, + pub b: WordRef, + pub m: [usize; 4], + pub s: [usize; 4], + pub c1: usize, + pub c2: usize, +} + +/// One recorded 2-op add: operands, output columns (carry is an expression). +pub(crate) struct Add2Wire { + pub a: WordRef, + pub b: WordRef, + pub s: [usize; 4], +} + +/// One recorded XOR: per-byte operands and output columns. +pub(crate) struct XorWire { + pub a: WordRef, + pub b: WordRef, + pub out: [usize; 4], +} + +/// One recorded shift rotation: input word, the 8 shift-halfword byte columns +/// (SLL_lo, SLLC_lo, SLL_hi, SLLC_hi — 2 bytes each), output columns, r. +pub(crate) struct RotWire { + pub input: WordRef, + pub sll_lo: [usize; 2], + pub sllc_lo: [usize; 2], + pub sll_hi: [usize; 2], + pub sllc_hi: [usize; 2], + pub y: [usize; 4], + pub r: u32, +} + +/// The full wiring of one compression row: everything the constraints and the +/// bus senders need, recorded in canonical order by [`run_flow`]. +pub(crate) struct WireFlow { + pub add3s: Vec, + pub add2s: Vec, + pub xors: Vec, + pub rots: Vec, +} + +impl WireFlow { + pub(crate) fn build() -> Self { + let mut w = WireFlow { + add3s: Vec::with_capacity(NUM_G * 2), + add2s: Vec::with_capacity(NUM_G * 2), + xors: Vec::with_capacity(NUM_G * 4 + 16), + rots: Vec::with_capacity(NUM_G * 2), + }; + run_flow(&mut w); + w + } +} + +#[inline] +fn word_cols(start: usize) -> [usize; 4] { + [start, start + 1, start + 2, start + 3] +} + +impl Blake3Flow for WireFlow { + type Word = WordRef; + + fn input_h(&mut self, i: usize) -> WordRef { + WordRef::Cols(word_cols(cols::in_word(i, 0))) + } + fn input_v12(&mut self, j: usize) -> WordRef { + WordRef::Cols(word_cols(cols::in_word(24 + j, 0))) + } + fn iv_const(&mut self, i: usize) -> WordRef { + WordRef::Const(BLAKE3_IV[i]) + } + + fn add3(&mut self, g: usize, half: usize, a: WordRef, b: WordRef, m_idx: usize) -> WordRef { + let base = cols::g_base(g) + if half == 0 { cols::G_A1 } else { cols::G_A2 }; + let cbase = cols::g_base(g) + + if half == 0 { + cols::G_A1_C + } else { + cols::G_A2_C + }; + let s = word_cols(base); + self.add3s.push(Add3Wire { + a, + b, + m: word_cols(cols::in_word(8 + m_idx, 0)), + s, + c1: cbase, + c2: cbase + 1, + }); + WordRef::Cols(s) + } + + fn add2(&mut self, g: usize, half: usize, a: WordRef, b: WordRef) -> WordRef { + let base = cols::g_base(g) + if half == 0 { cols::G_C1 } else { cols::G_C2 }; + let s = word_cols(base); + self.add2s.push(Add2Wire { a, b, s }); + WordRef::Cols(s) + } + + fn xor(&mut self, g: usize, slot: usize, a: WordRef, b: WordRef) -> WordRef { + let off = match slot { + 0 => cols::G_X1, + 1 => cols::G_X2, + 2 => cols::G_X3, + _ => cols::G_X4, + }; + let out = word_cols(cols::g_base(g) + off); + self.xors.push(XorWire { a, b, out }); + WordRef::Cols(out) + } + + fn rotr16(&mut self, w: WordRef) -> WordRef { + match w { + WordRef::Cols([b0, b1, b2, b3]) => WordRef::Cols([b2, b3, b0, b1]), + WordRef::Const(v) => WordRef::Const(v.rotate_right(16)), + } + } + fn rotr8(&mut self, w: WordRef) -> WordRef { + match w { + WordRef::Cols([b0, b1, b2, b3]) => WordRef::Cols([b1, b2, b3, b0]), + WordRef::Const(v) => WordRef::Const(v.rotate_right(8)), + } + } + + fn rot_shift(&mut self, g: usize, half: usize, w: WordRef) -> WordRef { + let base = cols::g_base(g) + if half == 0 { cols::G_R1 } else { cols::G_R2 }; + let y = word_cols(base + 8); + self.rots.push(RotWire { + input: w, + sll_lo: [base, base + 1], + sllc_lo: [base + 2, base + 3], + sll_hi: [base + 4, base + 5], + sllc_hi: [base + 6, base + 7], + y, + r: ROT_SHIFT_R[half], + }); + WordRef::Cols(y) + } + + fn feed_forward(&mut self, i: usize, vi: WordRef, vi8: WordRef, hi: WordRef) { + let out_lo = word_cols(cols::out_word(i, 0)); + let out_hi = word_cols(cols::out_word(i + 8, 0)); + self.xors.push(XorWire { + a: vi, + b: vi8, + out: out_lo, + }); + self.xors.push(XorWire { + a: vi8, + b: hi, + out: out_hi, + }); + } +} + +// ========================================================================= +// Value interpretation (u32 witness) +// ========================================================================= + +/// Everything the trace filler and the BITWISE collector need for one +/// compression, recorded cell-exactly in the same canonical order as +/// [`WireFlow`]. `xor_ops` carries (a, b) operand VALUES per XOR word — the +/// per-byte lookups are `(a_byte, b_byte)` in the same operand order the +/// senders use. +pub(crate) struct ValueFlow { + /// (s, c1, c2) per add3, canonical order. + pub add3s: Vec<(u32, u8, u8)>, + /// s per add2 (the carry is an expression, not a cell). + pub add2s: Vec, + /// (a, b, out) per XOR word, canonical order (Gs then feed-forward). + pub xors: Vec<(u32, u32, u32)>, + /// (sll_lo, sllc_lo, sll_hi, sllc_hi, y) per shift rotation. + pub rots: Vec<(u16, u16, u16, u16, u32)>, + /// The 16-word output. + pub out: [u32; 16], + + h: [u32; 8], + m: [u32; 16], + v12: [u32; 4], +} + +impl ValueFlow { + pub(crate) fn compute(h: &[u32; 8], m: &[u32; 16], t: u64, block_len: u32, flags: u32) -> Self { + let mut f = ValueFlow { + add3s: Vec::with_capacity(NUM_G * 2), + add2s: Vec::with_capacity(NUM_G * 2), + xors: Vec::with_capacity(NUM_G * 4 + 16), + rots: Vec::with_capacity(NUM_G * 2), + out: [0; 16], + h: *h, + m: *m, + v12: [t as u32, (t >> 32) as u32, block_len, flags], + }; + run_flow(&mut f); + f + } +} + +impl Blake3Flow for ValueFlow { + type Word = u32; + + fn input_h(&mut self, i: usize) -> u32 { + self.h[i] + } + fn input_v12(&mut self, j: usize) -> u32 { + self.v12[j] + } + fn iv_const(&mut self, i: usize) -> u32 { + BLAKE3_IV[i] + } + + fn add3(&mut self, _g: usize, _half: usize, a: u32, b: u32, m_idx: usize) -> u32 { + let m = self.m[m_idx]; + let wide = a as u64 + b as u64 + m as u64; + let s = wide as u32; + let carry = (wide >> 32) as u8; // 0, 1 or 2 + // Two summed carry bits: c1 + c2 = carry. + let (c1, c2) = match carry { + 0 => (0, 0), + 1 => (1, 0), + _ => (1, 1), + }; + self.add3s.push((s, c1, c2)); + s + } + + fn add2(&mut self, _g: usize, _half: usize, a: u32, b: u32) -> u32 { + let s = a.wrapping_add(b); + self.add2s.push(s); + s + } + + fn xor(&mut self, _g: usize, _slot: usize, a: u32, b: u32) -> u32 { + let out = a ^ b; + self.xors.push((a, b, out)); + out + } + + fn rotr16(&mut self, w: u32) -> u32 { + w.rotate_right(16) + } + fn rotr8(&mut self, w: u32) -> u32 { + w.rotate_right(8) + } + + fn rot_shift(&mut self, _g: usize, half: usize, w: u32) -> u32 { + let r = ROT_SHIFT_R[half]; + let xlo = w & 0xFFFF; + let xhi = w >> 16; + // xlo·2^r = SLLC_lo·2^16 + SLL_lo (and same for hi): Euclidean split. + let sll_lo = ((xlo << r) & 0xFFFF) as u16; + let sllc_lo = ((xlo << r) >> 16) as u16; + let sll_hi = ((xhi << r) & 0xFFFF) as u16; + let sllc_hi = ((xhi << r) >> 16) as u16; + // Recombine + halfword swap: Ylo = SLL_hi + SLLC_lo, Yhi = SLL_lo + SLLC_hi. + let ylo = sll_hi as u32 + sllc_lo as u32; + let yhi = sll_lo as u32 + sllc_hi as u32; + let y = ylo | (yhi << 16); + debug_assert_eq!(y, w.rotate_right(if r == 4 { 12 } else { 7 })); + self.rots.push((sll_lo, sllc_lo, sll_hi, sllc_hi, y)); + y + } + + fn feed_forward(&mut self, i: usize, vi: u32, vi8: u32, hi: u32) { + let lo = vi ^ vi8; + let hi_w = vi8 ^ hi; + self.xors.push((vi, vi8, lo)); + self.xors.push((vi8, hi, hi_w)); + self.out[i] = lo; + self.out[i + 8] = hi_w; + } +} + +// ========================================================================= +// Operation struct + trace generation +// ========================================================================= + +#[derive(Debug, Clone)] +pub struct Blake3Operation { + pub timestamp: u64, + pub state_addr: u64, + pub h: [u32; 8], + pub m: [u32; 16], + pub t: u64, + pub block_len: u32, + pub flags: u32, + /// Previous memory content of the 64-byte out region (for the Memw `old`). + pub old_out: [u8; 64], + /// The 16-word compression output (recomputed by the trace builder). + pub out: [u32; 16], +} + +/// Write a 32-bit word as 4 byte cells at `col..col+4`. +#[inline] +fn set_word_bytes(table: &mut T, row: usize, col: usize, w: u32) { + for b in 0..4 { + table.set_u64(row, col + b, ((w >> (8 * b)) & 0xFF) as u64); + } +} + +pub fn generate_blake3_trace( + ops: &[Blake3Operation], +) -> TraceTable { + let n = ops.len(); + let num_rows = n.next_power_of_two().max(4); + let mut trace = TraceTable::new_main( + crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS), + cols::NUM_COLUMNS, + 1, + ); + let table = &mut trace.main_table; + + for (row, op) in ops.iter().enumerate() { + table.set_dword_wl(row, cols::TIMESTAMP_0, op.timestamp); + table.set_dword_bl(row, cols::addr(0), op.state_addr); + + // Pointers ptr[k] = addr + 8k. + for k in 0..STATE_DWORDS { + let ptr = op + .state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + table.set_dword_hl(row, cols::ptr(k, 0), ptr); + } + + // Input words: h | m | t_lo t_hi len flags. + for i in 0..8 { + set_word_bytes(table, row, cols::in_word(i, 0), op.h[i]); + } + for i in 0..16 { + set_word_bytes(table, row, cols::in_word(8 + i, 0), op.m[i]); + } + set_word_bytes(table, row, cols::in_word(24, 0), op.t as u32); + set_word_bytes(table, row, cols::in_word(25, 0), (op.t >> 32) as u32); + set_word_bytes(table, row, cols::in_word(26, 0), op.block_len); + set_word_bytes(table, row, cols::in_word(27, 0), op.flags); + + // The mixing core, cell-exactly in canonical order. + let flow = ValueFlow::compute(&op.h, &op.m, op.t, op.block_len, op.flags); + debug_assert_eq!( + flow.out, op.out, + "trace-builder output must match the executor" + ); + + let mut a3 = flow.add3s.iter(); + let mut a2 = flow.add2s.iter(); + let mut xo = flow.xors.iter(); + let mut ro = flow.rots.iter(); + for g in 0..NUM_G { + let base = cols::g_base(g); + for half in 0..2 { + let (s_off, c_off, x_off, c2_off, x2_off, r_off) = if half == 0 { + ( + cols::G_A1, + cols::G_A1_C, + cols::G_X1, + cols::G_C1, + cols::G_X2, + cols::G_R1, + ) + } else { + ( + cols::G_A2, + cols::G_A2_C, + cols::G_X3, + cols::G_C2, + cols::G_X4, + cols::G_R2, + ) + }; + let &(s, c1, c2) = a3.next().expect("add3 count"); + set_word_bytes(table, row, base + s_off, s); + table.set_u64(row, base + c_off, c1 as u64); + table.set_u64(row, base + c_off + 1, c2 as u64); + + let &(_, _, x) = xo.next().expect("xor count"); + set_word_bytes(table, row, base + x_off, x); + + let &c = a2.next().expect("add2 count"); + set_word_bytes(table, row, base + c2_off, c); + + let &(_, _, x2) = xo.next().expect("xor count"); + set_word_bytes(table, row, base + x2_off, x2); + + let &(sll_lo, sllc_lo, sll_hi, sllc_hi, y) = ro.next().expect("rot count"); + table.set_u64(row, base + r_off, (sll_lo & 0xFF) as u64); + table.set_u64(row, base + r_off + 1, (sll_lo >> 8) as u64); + table.set_u64(row, base + r_off + 2, (sllc_lo & 0xFF) as u64); + table.set_u64(row, base + r_off + 3, (sllc_lo >> 8) as u64); + table.set_u64(row, base + r_off + 4, (sll_hi & 0xFF) as u64); + table.set_u64(row, base + r_off + 5, (sll_hi >> 8) as u64); + table.set_u64(row, base + r_off + 6, (sllc_hi & 0xFF) as u64); + table.set_u64(row, base + r_off + 7, (sllc_hi >> 8) as u64); + set_word_bytes(table, row, base + r_off + 8, y); + } + } + // Feed-forward outputs (the last 16 entries of flow.xors). + for i in 0..16 { + set_word_bytes(table, row, cols::out_word(i, 0), flow.out[i]); + } + // Previous content of the out region. + for b in 0..64 { + table.set_u64(row, cols::old_out(b), op.old_out[b] as u64); + } + + table.set_fe(row, cols::MU, FE::one()); + } + + // Padding rows: ptr[k][0] = 8k (all fit in the low halfword), matching the + // keccak pad idiom. μ = 0 gates every constraint and interaction. + for row in n..num_rows { + for k in 0..STATE_DWORDS { + table.set_u64(row, cols::ptr(k, 0), (k as u64) * 8); + } + } + + trace +} + +// ========================================================================= +// Bus interactions +// ========================================================================= + +/// Order groups: I/O (Ecall + reg-read + 22 Memw), then the mixing core's +/// ByteAlu XORs (canonical WireFlow order), then the shift AreBytes, then the +/// message/old-out/addr AreBytes, the alignment AND and the pointer IS_HALFs. +pub fn bus_interactions() -> Vec { + let syscall_lo = BLAKE3_SYSCALL_NUMBER & 0xFFFF_FFFF; + let syscall_hi = BLAKE3_SYSCALL_NUMBER >> 32; + let wires = WireFlow::build(); + let mut interactions = Vec::with_capacity(1400); + + let byte_bus_value = |b: ByteRef| -> BusValue { + match b { + ByteRef::Col(c) => BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }, + ByteRef::Const(v) => BusValue::constant(v as u64), + } + }; + + // 1. ECALL receiver: [ts_lo, ts_hi, syscall_lo32, syscall_hi32]. + interactions.push(BusInteraction::receiver( + BusId::Ecall, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::TIMESTAMP_0, + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::TIMESTAMP_1, + packing: Packing::Direct, + }, + BusValue::constant(syscall_lo), + BusValue::constant(syscall_hi), + ], + )); + + // 2. MEMW read of register x10 binding the state address (keccak idiom): + // [old(8), is_register=1, base=20, value(8), ts(2), w2=1, w4=0, w8=0]. + { + let addr_word = |lo_byte: usize| -> BusValue { + BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::addr(lo_byte), + }, + LinearTerm::Column { + coefficient: 256, + column: cols::addr(lo_byte + 1), + }, + LinearTerm::Column { + coefficient: 65536, + column: cols::addr(lo_byte + 2), + }, + LinearTerm::Column { + coefficient: 16777216, + column: cols::addr(lo_byte + 3), + }, + ]) + }; + let mut values = Vec::with_capacity(24); + values.push(addr_word(0)); + values.push(addr_word(4)); + for _ in 2..8 { + values.push(BusValue::constant(0)); + } + values.push(BusValue::constant(1)); // is_register + values.push(BusValue::constant(20)); // x10 → address 2*10 + values.push(BusValue::constant(0)); + values.push(addr_word(0)); + values.push(addr_word(4)); + for _ in 2..8 { + values.push(BusValue::constant(0)); + } + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_0, + packing: Packing::Direct, + }); + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_1, + packing: Packing::Direct, + }); + values.push(BusValue::constant(1)); // w2 (register) + values.push(BusValue::constant(0)); + values.push(BusValue::constant(0)); + interactions.push(BusInteraction::sender( + BusId::Memw, + Multiplicity::Column(cols::MU), + values, + )); + } + + // 3. MEMW per state dword: [old(8), is_register=0, addr(2), value(8), ts(2), + // w2=0, w4=0, w8=1]. Input dwords are pure reads (old = value = input + // bytes); output dwords write OUT over OLD_OUT. + for k in 0..STATE_DWORDS { + let addr_lo = BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::ptr(k, 0), + }, + LinearTerm::Column { + coefficient: 65536, + column: cols::ptr(k, 1), + }, + ]); + let addr_hi = BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::ptr(k, 2), + }, + LinearTerm::Column { + coefficient: 65536, + column: cols::ptr(k, 3), + }, + ]); + + // (old bytes, value bytes) column bases for this dword. + let (old_base, val_base): (Vec, Vec) = if k < IN_DWORDS { + let cols8: Vec = (0..8).map(|b| cols::in_word(2 * k, 0) + b).collect(); + (cols8.clone(), cols8) + } else { + let o = k - IN_DWORDS; + ( + (0..8).map(|b| cols::old_out(o * 8 + b)).collect(), + (0..8).map(|b| cols::out_word(2 * o, 0) + b).collect(), + ) + }; + + let mut values = Vec::with_capacity(24); + for &c in &old_base { + values.push(BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }); + } + values.push(BusValue::constant(0)); // is_register + values.push(addr_lo); + values.push(addr_hi); + for &c in &val_base { + values.push(BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }); + } + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_0, + packing: Packing::Direct, + }); + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_1, + packing: Packing::Direct, + }); + values.push(BusValue::constant(0)); + values.push(BusValue::constant(0)); + values.push(BusValue::constant(1)); // w8 + interactions.push(BusInteraction::sender( + BusId::Memw, + Multiplicity::Column(cols::MU), + values, + )); + } + + // 4. Mixing core + feed-forward: ByteAlu[XOR] per byte, canonical order. + for xw in &wires.xors { + for b in 0..4 { + interactions.push(BusInteraction::sender( + BusId::ByteAlu, + Multiplicity::Column(cols::MU), + vec![ + BusValue::constant(alu_op::XOR as u64), + byte_bus_value(xw.a.byte(b)), + byte_bus_value(xw.b.byte(b)), + BusValue::Packed { + start_column: xw.out[b], + packing: Packing::Direct, + }, + ], + )); + } + } + + // 5. Shift-halfword AreBytes: 4 pairs per rotation + // (SLL_lo, SLLC_lo, SLL_hi, SLLC_hi bytes). + for rw in &wires.rots { + for pair in [rw.sll_lo, rw.sllc_lo, rw.sll_hi, rw.sllc_hi] { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: pair[0], + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: pair[1], + packing: Packing::Direct, + }, + ], + )); + } + } + + // 6. Message AreBytes (m is never XORed — DESIGN §4.7/§7.5): 32 pairs. + for i in 0..16 { + for p in 0..2 { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::in_word(8 + i, 2 * p), + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::in_word(8 + i, 2 * p + 1), + packing: Packing::Direct, + }, + ], + )); + } + } + + // 7. OLD_OUT AreBytes: those bytes only ride the Memw bus; without a byte + // range check their packed linear combinations alias (same argument as the + // addr bytes in keccak.rs). + for p in 0..32 { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::old_out(2 * p), + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::old_out(2 * p + 1), + packing: Packing::Direct, + }, + ], + )); + } + + // 8. Address byte range checks (4 pairs) + alignment addr[0] & 7 = 0. + for i in 0..4 { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::addr(2 * i), + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::addr(2 * i + 1), + packing: Packing::Direct, + }, + ], + )); + } + interactions.push(BusInteraction::sender( + BusId::ByteAlu, + Multiplicity::Column(cols::MU), + vec![ + BusValue::constant(alu_op::AND as u64), + BusValue::Packed { + start_column: cols::addr(0), + packing: Packing::Direct, + }, + BusValue::constant(7), + BusValue::constant(0), + ], + )); + + // 9. IS_HALF range checks on the 22 pointers' halfwords. + for k in 0..STATE_DWORDS { + for hw in 0..4 { + interactions.push(BusInteraction::sender( + BusId::IsHalfword, + Multiplicity::Column(cols::MU), + vec![BusValue::Packed { + start_column: cols::ptr(k, hw), + packing: Packing::Direct, + }], + )); + } + } + + interactions +} + +// ========================================================================= +// Single-source constraint set +// ========================================================================= + +/// The BLAKE3 table's transition constraints: +/// - idx 0..44: 22 pointer `ADD` carry pairs (`ptr[k] = addr + 8k`, μ-gated); +/// - idx 44: μ·carry_1 = 0 — top-dword no-overflow (`addr + 168 = ptr[21]`); +/// - idx 45...: per G (16 each): two add3 groups (sum identity + 2 carry +/// booleanities), two add2 expression-carry booleanities, two rotations +/// (2 shift identities + 2 recombine identities each). All μ-gated, max +/// degree 3 (the booleanities; identities are degree 2). +#[derive(Clone, Copy)] +pub struct Blake3Constraints; + +/// Word expression from a [`WordRef`]: b0 + 256·b1 + 2^16·b2 + 2^24·b3. +fn word_expr>( + b: &B, + w: &WordRef, +) -> B::Expr { + match w { + WordRef::Cols(c) => { + b.main(0, c[0]) + + b.main(0, c[1]) * b.const_base(256) + + b.main(0, c[2]) * b.const_base(65536) + + b.main(0, c[3]) * b.const_base(16777216) + } + WordRef::Const(v) => b.const_base(*v as u64), + } +} + +/// Halfword expression from 2 byte columns: b0 + 256·b1. +fn half_expr>( + b: &B, + c: &[usize; 2], +) -> B::Expr { + b.main(0, c[0]) + b.main(0, c[1]) * b.const_base(256) +} + +impl ConstraintSet for Blake3Constraints { + fn max_degree(&self) -> usize { + 3 + } + + fn eval>(&self, b: &mut B) { + use crate::constraints::templates::emit_add_pair; + + let wires = WireFlow::build(); + let mu = |b: &B| b.main(0, cols::MU); + + // idx 0..44: ptr[k] = addr + 8k (μ-gated carry pairs). + for k in 0..STATE_DWORDS { + emit_add_pair( + b, + k * 2, + &[cols::MU], + &AddOperand::from_dword_bl(cols::ADDR), + &AddOperand::constant((k * 8) as i64), + &AddOperand::from_dword_hl(cols::ptr(k, 0)), + ); + } + + // idx 44: top-dword no-overflow — μ·carry_1 of addr + 168 = ptr[21]. + let mut idx = STATE_DWORDS * 2; + { + let c256 = b.const_base(256); + let c65536 = b.const_base(65536); + let c16777216 = b.const_base(16777216); + let addr_lo = b.main(0, cols::addr(0)) + + b.main(0, cols::addr(1)) * c256.clone() + + b.main(0, cols::addr(2)) * c65536.clone() + + b.main(0, cols::addr(3)) * c16777216.clone(); + let addr_hi = b.main(0, cols::addr(4)) + + b.main(0, cols::addr(5)) * c256 + + b.main(0, cols::addr(6)) * c65536.clone() + + b.main(0, cols::addr(7)) * c16777216; + let last = STATE_DWORDS - 1; + let ptr_lo = + b.main(0, cols::ptr(last, 0)) + b.main(0, cols::ptr(last, 1)) * c65536.clone(); + let ptr_hi = b.main(0, cols::ptr(last, 2)) + b.main(0, cols::ptr(last, 3)) * c65536; + + let inv_2_32 = b.const_base(INV_SHIFT_32); + let off = b.const_base((8 * last) as u64); + let carry_0 = (addr_lo + off - ptr_lo) * inv_2_32.clone(); + let carry_1 = (addr_hi + carry_0 - ptr_hi) * inv_2_32; + let m = mu(b); + b.emit_base(idx, m * carry_1); + idx += 1; + } + + // Mixing core. Same canonical order as the wire builder records. + let two_32 = b.const_base(1u64 << 32); + let inv_2_32 = b.const_base(INV_SHIFT_32); + + // add3: μ·(a + b + m − s − 2^32·(c1+c2)) = 0; μ·ci·(1−ci) = 0. + for aw in &wires.add3s { + let a = word_expr(b, &aw.a); + let bb = word_expr(b, &aw.b); + let m_w = word_expr(b, &WordRef::Cols(aw.m)); + let s = word_expr(b, &WordRef::Cols(aw.s)); + let c1 = b.main(0, aw.c1); + let c2 = b.main(0, aw.c2); + let sum_id = a + bb + m_w - s - (c1.clone() + c2.clone()) * two_32.clone(); + let m = mu(b); + b.emit_base(idx, m * sum_id); + idx += 1; + let one = b.one(); + let m = mu(b); + b.emit_base(idx, m * c1.clone() * (one - c1)); + idx += 1; + let one = b.one(); + let m = mu(b); + b.emit_base(idx, m * c2.clone() * (one - c2)); + idx += 1; + } + + // add2: carry = (a + b − s)·2^−32; μ·carry·(1−carry) = 0. + for aw in &wires.add2s { + let a = word_expr(b, &aw.a); + let bb = word_expr(b, &aw.b); + let s = word_expr(b, &WordRef::Cols(aw.s)); + let carry = (a + bb - s) * inv_2_32.clone(); + let one = b.one(); + let m = mu(b); + b.emit_base(idx, m * carry.clone() * (one - carry)); + idx += 1; + } + + // Rotations: 2 shift identities + 2 recombine identities each. + for rw in &wires.rots { + let (xlo, xhi) = match &rw.input { + WordRef::Cols(c) => (half_expr(b, &[c[0], c[1]]), half_expr(b, &[c[2], c[3]])), + WordRef::Const(_) => unreachable!("shift inputs are always committed XOR outputs"), + }; + let sll_lo = half_expr(b, &rw.sll_lo); + let sllc_lo = half_expr(b, &rw.sllc_lo); + let sll_hi = half_expr(b, &rw.sll_hi); + let sllc_hi = half_expr(b, &rw.sllc_hi); + let ylo = half_expr(b, &[rw.y[0], rw.y[1]]); + let yhi = half_expr(b, &[rw.y[2], rw.y[3]]); + let two_r = b.const_base(1u64 << rw.r); + let two_16 = b.const_base(65536); + + // μ·(xlo·2^r − SLLC_lo·2^16 − SLL_lo) = 0 (and hi). + let m = mu(b); + b.emit_base( + idx, + m * (xlo * two_r.clone() - sllc_lo.clone() * two_16.clone() - sll_lo.clone()), + ); + idx += 1; + let m = mu(b); + b.emit_base( + idx, + m * (xhi * two_r - sllc_hi.clone() * two_16 - sll_hi.clone()), + ); + idx += 1; + // μ·(Ylo − SLL_hi − SLLC_lo) = 0; μ·(Yhi − SLL_lo − SLLC_hi) = 0. + let m = mu(b); + b.emit_base(idx, m * (ylo - sll_hi - sllc_lo)); + idx += 1; + let m = mu(b); + b.emit_base(idx, m * (yhi - sll_lo - sllc_hi)); + idx += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The value interpretation must reproduce the executor's compression — + /// same function the canonical oracle vectors validate. + #[test] + fn value_flow_matches_executor() { + use executor::vm::instruction::execution::blake3_compress_6round; + let h: [u32; 8] = core::array::from_fn(|i| 0x9E3779B9u32.wrapping_mul(i as u32 + 1)); + let m: [u32; 16] = core::array::from_fn(|i| 0x85EBCA6Bu32.wrapping_mul(i as u32 + 7)); + let t = 0x0123_4567_89AB_CDEFu64; + let (bl, fl) = (64u32, 11u32); + let flow = ValueFlow::compute(&h, &m, t, bl, fl); + assert_eq!(flow.out, blake3_compress_6round(&h, &m, t, bl, fl)); + } + + /// Canonical op counts: 96 add3s, 96 add2s, 96 rotations, 192+16 XORs. + #[test] + fn wire_flow_counts() { + let w = WireFlow::build(); + assert_eq!(w.add3s.len(), NUM_G * 2); + assert_eq!(w.add2s.len(), NUM_G * 2); + assert_eq!(w.rots.len(), NUM_G * 2); + assert_eq!(w.xors.len(), NUM_G * 4 + 16); + // Every output column lands exactly once, and inside the row. + use std::collections::HashSet; + let mut seen = HashSet::new(); + let mut claim = |c: usize| { + assert!(c < cols::NUM_COLUMNS, "column {c} out of range"); + assert!(seen.insert(c), "column {c} written twice"); + }; + for aw in &w.add3s { + for c in aw.s { + claim(c); + } + claim(aw.c1); + claim(aw.c2); + } + for aw in &w.add2s { + for c in aw.s { + claim(c); + } + } + for xw in &w.xors { + for c in xw.out { + claim(c); + } + } + for rw in &w.rots { + for c in rw + .sll_lo + .iter() + .chain(&rw.sllc_lo) + .chain(&rw.sll_hi) + .chain(&rw.sllc_hi) + .chain(&rw.y) + { + claim(*c); + } + } + // 48 G-blocks × 60 cells + 64 out bytes, all distinct. + assert_eq!(seen.len(), NUM_G * cols::G_SIZE + 64); + } +} diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 781bb02b0..4add10639 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -185,6 +185,10 @@ pub struct CpuOperation { pub ecall_keccak: bool, /// For KeccakPermute ECALLs: state address from x10. pub keccak_state_addr: u64, + /// Whether this ECALL is a Blake3Compress syscall. + pub ecall_blake3: bool, + /// For Blake3Compress ECALLs: state address from x10. + pub blake3_state_addr: u64, /// Whether this ECALL is an ECSM (elliptic-curve scalar multiply) syscall pub ecall_ecsm: bool, @@ -231,6 +235,9 @@ impl CpuOperation { let ecall_keccak = f.ecall && log.src1_val == executor::vm::instruction::execution::KECCAK_SYSCALL_NUMBER; let keccak_state_addr = if ecall_keccak { log.src2_val } else { 0 }; + let ecall_blake3 = + f.ecall && log.src1_val == executor::vm::instruction::execution::BLAKE3_SYSCALL_NUMBER; + let blake3_state_addr = if ecall_blake3 { log.src2_val } else { 0 }; // The ECSM operand addresses (x10/x11/x12) are recovered from the register state // in the trace builder. let ecall_ecsm = @@ -252,6 +259,8 @@ impl CpuOperation { commit_count, ecall_keccak, keccak_state_addr, + ecall_blake3, + blake3_state_addr, decode, timestamp, ..Default::default() @@ -352,6 +361,8 @@ impl CpuOperation { commit_count, ecall_keccak, keccak_state_addr, + ecall_blake3, + blake3_state_addr, ecall_ecsm, } } diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 0a86e4149..784f83a96 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -22,6 +22,7 @@ pub mod types; pub mod bitwise; +pub mod blake3; pub mod branch; pub mod bytewise; pub mod commit; diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 5ec9fa566..9fc183e54 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -40,6 +40,7 @@ use stark::storage_mode::StorageMode; use stark::trace::TraceTable; use super::bitwise::{self, BitwiseOperation, BitwiseOperationType}; +use super::blake3::{self, Blake3Operation}; use super::branch::{self, BranchOperation}; use super::bytewise; use super::commit::{self, CommitOperation}; @@ -546,6 +547,7 @@ fn collect_ops_from_cpu( Vec, Vec, Vec, + Vec, Vec, Vec, Vec, @@ -557,6 +559,7 @@ fn collect_ops_from_cpu( let mut bitwise_ops = Vec::with_capacity(cpu_ops.len() * 4); let mut commit_ops = Vec::new(); let mut keccak_ops = Vec::new(); + let mut blake3_ops = Vec::new(); let mut cpu32_ops = Vec::new(); let mut ecsm_ops = Vec::new(); let mut ecdas_ops = Vec::new(); @@ -645,6 +648,60 @@ fn collect_ops_from_cpu( }); } + // Collect Blake3Compress ECALL operations + if op.ecall_blake3 { + let state_addr = op.blake3_state_addr; + // 14 input dwords: h | m | t | (block_len, flags), LE words. + let mut words = [0u32; 28]; + for k in 0..14usize { + let dword_addr = state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + let mut dw = 0u64; + for b in 0..8 { + let byte_addr = dword_addr + .checked_add(b as u64) + .expect("blake3 state address range must be validated by the executor"); + let (byte_val, _ts) = memory_state.read_byte(byte_addr); + dw |= (byte_val as u64) << (b * 8); + } + words[2 * k] = dw as u32; + words[2 * k + 1] = (dw >> 32) as u32; + } + let h: [u32; 8] = words[0..8].try_into().unwrap(); + let m: [u32; 16] = words[8..24].try_into().unwrap(); + let t = (words[24] as u64) | ((words[25] as u64) << 32); + let block_len = words[26]; + let flags = words[27]; + let out = executor::vm::instruction::execution::blake3_compress_6round( + &h, &m, t, block_len, flags, + ); + // Previous content of the out region, read BEFORE the write ops + // below advance memory_state. + let mut old_out = [0u8; 64]; + for (b, byte) in old_out.iter_mut().enumerate() { + let byte_addr = state_addr + .checked_add(112 + b as u64) + .expect("blake3 state address range must be validated by the executor"); + let (v, _ts) = memory_state.read_byte(byte_addr); + *byte = v; + } + let blake3_memw_ops = + collect_blake3_memw_ops(op, &words, &out, memory_state, register_state); + memw.extend_ops(blake3_memw_ops); + blake3_ops.push(Blake3Operation { + timestamp: op.timestamp, + state_addr, + h, + m, + t, + block_len, + flags, + old_out, + out, + }); + } + // Collect ECSM ecall operations (memory I/O + the two table row sets) if op.ecall_ecsm { let (ecsm_memw, ecsm_op, ecdas_rows) = @@ -706,6 +763,7 @@ fn collect_ops_from_cpu( bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -1417,6 +1475,79 @@ fn collect_keccak_memw_ops( memw_ops } +/// Collect MEMW operations for a Blake3Compress ECALL. +/// +/// One register read of x10 plus 22 dword ops at the call's timestamp: the 14 +/// input dwords are pure reads (old = value = the input bytes, re-written at +/// `ts` like a LOAD), the 8 output dwords write the compression output over +/// the previous content. +fn collect_blake3_memw_ops( + op: &CpuOperation, + words: &[u32; 28], + out: &[u32; 16], + memory_state: &mut MemoryState, + register_state: &mut RegisterState, +) -> Vec { + let ts = op.timestamp; + let state_addr = op.blake3_state_addr; + let mut memw_ops = Vec::with_capacity(23); // 1 register read + 22 dword ops + + // Read register x10 to bind state_addr (same as keccak:c:read_addr). + { + let reg_value = pack_register_value(state_addr); + let reg_addr = 2 * 10u64; // x10 -> address 20 + let (_old_val, old_ts) = register_state.read(10); + let old_timestamps = [old_ts, old_ts, 0, 0, 0, 0, 0, 0]; + let memw_op = MemwOperation::new(true, reg_addr, reg_value, ts, 2, true) + .with_old(reg_value, old_timestamps); + memw_ops.push(memw_op); + register_state.write(10, state_addr, ts); + } + + for k in 0..22usize { + let dword_addr = state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + + // The dword's new value: input dwords re-write their own bytes, output + // dwords write the compression output. + let dw = if k < 14 { + (words[2 * k] as u64) | ((words[2 * k + 1] as u64) << 32) + } else { + let o = k - 14; + (out[2 * o] as u64) | ((out[2 * o + 1] as u64) << 32) + }; + let mut value_bytes = [0u32; 8]; + for (b, byte) in value_bytes.iter_mut().enumerate() { + *byte = ((dw >> (b * 8)) & 0xFF) as u32; + } + + let mut old_bytes = [0u32; 8]; + let mut old_timestamps = [0u64; 8]; + for b in 0..8 { + let byte_addr = dword_addr + .checked_add(b as u64) + .expect("blake3 state address range must be validated by the executor"); + let (old_val, old_ts) = memory_state.read_byte(byte_addr); + old_bytes[b] = old_val as u32; + old_timestamps[b] = old_ts; + } + + let memw_op = MemwOperation::new(false, dword_addr, value_bytes, ts, 8, true) + .with_old(old_bytes, old_timestamps); + memw_ops.push(memw_op); + + for (b, &val) in value_bytes.iter().enumerate() { + let byte_addr = dword_addr + .checked_add(b as u64) + .expect("blake3 state address range must be validated by the executor"); + memory_state.write_byte(byte_addr, val as u8, ts); + } + } + + memw_ops +} + /// /// From spec memw.md: /// - MEMW-C4 through MEMW-C7: old_timestamp[i] < timestamp (based on width) @@ -2334,6 +2465,97 @@ pub(crate) fn collect_bitwise_from_ecdas(ops: &[ecdas::EcdasOperation]) -> Vec Vec { + let mut ops = Vec::new(); + + for bop in blake3_ops { + let state_addr = bop.state_addr; + + // Alignment: addr[0] & 7 = 0. + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::ByteAluAnd, + (state_addr & 0xFF) as u8, + 7, + )); + + // Addr byte range checks: (addr[2i], addr[2i+1]) pairs. + for i in 0..4 { + let lo = ((state_addr >> (2 * i * 8)) & 0xFF) as u8; + let hi = ((state_addr >> ((2 * i + 1) * 8)) & 0xFF) as u8; + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + lo, + hi, + )); + } + + // IS_HALF for the 22 pointers' halfwords. + for k in 0..blake3::STATE_DWORDS { + let ptr = state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + for shift in [0, 16, 32, 48] { + let half = ((ptr >> shift) & 0xFFFF) as u16; + ops.push(BitwiseOperation::halfword( + BitwiseOperationType::IsHalf, + (half & 0xFF) as u8, + ((half >> 8) & 0xFF) as u8, + )); + } + } + + // Mixing core + feed-forward, in the senders' canonical order. + let flow = blake3::ValueFlow::compute(&bop.h, &bop.m, bop.t, bop.block_len, bop.flags); + for &(a, b, _out) in &flow.xors { + for byte in 0..4 { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::ByteAluXor, + ((a >> (8 * byte)) & 0xFF) as u8, + ((b >> (8 * byte)) & 0xFF) as u8, + )); + } + } + for &(sll_lo, sllc_lo, sll_hi, sllc_hi, _y) in &flow.rots { + for hw in [sll_lo, sllc_lo, sll_hi, sllc_hi] { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + (hw & 0xFF) as u8, + (hw >> 8) as u8, + )); + } + } + + // Message AreBytes: (byte 2p, byte 2p+1) of each m word. + for m in bop.m { + for p in 0..2 { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + ((m >> (16 * p)) & 0xFF) as u8, + ((m >> (16 * p + 8)) & 0xFF) as u8, + )); + } + } + + // OLD_OUT AreBytes pairs. + for p in 0..32 { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + bop.old_out[2 * p], + bop.old_out[2 * p + 1], + )); + } + } + + ops +} + /// Collect BITWISE lookups generated by the keccak chips. /// /// The keccak round chip sends BYTE_ALU and ARE_BYTES interactions (the θ/ρ @@ -2761,6 +2983,9 @@ pub struct Traces { /// KECCAK_RC precomputed round constant table (32 rows) pub keccak_rc: TraceTable, + /// BLAKE3 6-round compression table (one row per compression call) + pub blake3: TraceTable, + /// ECSM core table (one row per scalar-multiplication ecall) pub ecsm: TraceTable, @@ -2801,6 +3026,7 @@ struct CollectedOps { dvrm_ops: Vec<(DvrmOperation, bool)>, commit_ops: Vec, keccak_ops: Vec, + blake3_ops: Vec, // Auxiliary ALU / memory / CPU32 dispatch chips (driven by the CPU ALU/MEMORY dispatch). eq_ops: Vec, bytewise_ops: Vec, @@ -2860,6 +3086,7 @@ fn collect_all_ops( mut bitwise_ops: Vec, commit_ops: Vec, keccak_ops: Vec, + blake3_ops: Vec, cpu32_ops: Vec, ecsm_ops: Vec, ecdas_ops: Vec, @@ -2999,6 +3226,7 @@ fn collect_all_ops( dvrm_ops, commit_ops, keccak_ops, + blake3_ops, eq_ops, bytewise_ops, store_ops, @@ -3042,6 +3270,7 @@ fn build_traces( dvrm_ops, commit_ops, keccak_ops, + blake3_ops, eq_ops, bytewise_ops, store_ops, @@ -3121,6 +3350,7 @@ fn build_traces( Box::new(|h| h.add_ops(&collect_bitwise_from_memw_aligned(&memw_aligned_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_commit(&commit_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_keccak(&keccak_ops))), + Box::new(|h| h.add_ops(&collect_bitwise_from_blake3(&blake3_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecsm(&ecsm_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecdas(&ecdas_ops))), Box::new(|h| add_padding_byte_checks(h, num_padding_rows)), @@ -3390,6 +3620,7 @@ fn build_traces( .collect(); keccak_rnd::generate_keccak_rnd_trace(&keccak_rnd_ops) }; + let gen_blake3 = || blake3::generate_blake3_trace(&blake3_ops); let gen_keccak_rc = || { let mut keccak_rc_trace = keccak_rc::generate_keccak_rc_trace(); keccak_rc::update_multiplicities(&mut keccak_rc_trace, keccak_ops.len()); @@ -3417,6 +3648,7 @@ fn build_traces( (None, None, None, None); let (mut commit_slot, mut keccak_slot, mut keccak_rnd_slot, mut keccak_rc_slot) = (None, None, None, None); + let mut blake3_slot = None; let (mut pages_slot, mut register_slot, mut halt_slot) = (None, None, None); let (mut eqs_slot, mut bytewises_slot, mut stores_slot, mut cpu32s_slot) = (None, None, None, None); @@ -3453,6 +3685,7 @@ fn build_traces( spawn_into!(keccak_slot, gen_keccak); spawn_into!(keccak_rnd_slot, gen_keccak_rnd); spawn_into!(keccak_rc_slot, gen_keccak_rc); + spawn_into!(blake3_slot, gen_blake3); spawn_into!(commit_slot, gen_commit); spawn_into!(register_slot, gen_register); spawn_into!(halt_slot, gen_halt); @@ -3480,6 +3713,7 @@ fn build_traces( keccak_slot = Some(gen_keccak()); keccak_rnd_slot = Some(gen_keccak_rnd()); keccak_rc_slot = Some(gen_keccak_rc()); + blake3_slot = Some(gen_blake3()); pages_slot = Some(gen_pages()); register_slot = Some(gen_register()); halt_slot = Some(gen_halt()); @@ -3515,6 +3749,7 @@ fn build_traces( let keccak_trace = keccak_slot.expect(PHASE5_RAN); let keccak_rnd_trace = keccak_rnd_slot.expect(PHASE5_RAN); let keccak_rc_trace = keccak_rc_slot.expect(PHASE5_RAN); + let blake3_trace = blake3_slot.expect(PHASE5_RAN); #[allow(unused_mut)] let (mut pages, page_configs) = pages_slot.expect(PHASE5_RAN); #[allow(unused_mut)] @@ -3587,6 +3822,7 @@ fn build_traces( commit: commit_trace, keccak: keccak_trace, keccak_rnd: keccak_rnd_trace, + blake3: blake3_trace, keccak_rc: keccak_rc_trace, ecsm: ecsm_trace, ecdas: ecdas_trace, @@ -3840,6 +4076,7 @@ impl Traces { pub fn total_field_elements(&self) -> u64 { use super::bitwise::NUM_PRECOMPUTED_COLS as BITWISE_PRECOMPUTED; use super::bitwise::cols::NUM_COLUMNS as BITWISE_COLS; + use super::blake3::cols::NUM_COLUMNS as BLAKE3_COLS; use super::branch::cols::NUM_COLUMNS as BRANCH_COLS; use super::bytewise::cols::NUM_COLUMNS as BYTEWISE_COLS; use super::commit::cols::NUM_COLUMNS as COMMIT_COLS; @@ -3888,6 +4125,7 @@ impl Traces { keccak, keccak_rnd, keccak_rc, + blake3, ecsm, ecdas, memw_registers, @@ -3943,6 +4181,7 @@ impl Traces { total += (keccak.num_rows() * KECCAK_COLS) as u64; total += (keccak_rnd.num_rows() * KECCAK_RND_COLS) as u64; total += (keccak_rc.num_rows() * (KECCAK_RC_COLS - KECCAK_RC_PRECOMPUTED)) as u64; + total += (blake3.num_rows() * BLAKE3_COLS) as u64; for t in eqs { total += (t.num_rows() * EQ_COLS) as u64; } @@ -3992,6 +4231,7 @@ impl Traces { let n_keccak = aux_cols(super::keccak::bus_interactions().len()); let n_keccak_rnd = aux_cols(super::keccak_rnd::bus_interactions().len()); let n_keccak_rc = aux_cols(super::keccak_rc::bus_interactions().len()); + let n_blake3 = aux_cols(super::blake3::bus_interactions().len()); let n_eq = aux_cols(super::eq::bus_interactions().len()); let n_bytewise = aux_cols(super::bytewise::bus_interactions().len()); let n_store = aux_cols(super::store::bus_interactions().len()); @@ -4018,6 +4258,7 @@ impl Traces { keccak, keccak_rnd, keccak_rc, + blake3, ecsm, ecdas, memw_registers, @@ -4073,6 +4314,7 @@ impl Traces { total += (keccak.num_rows() * n_keccak) as u64; total += (keccak_rnd.num_rows() * n_keccak_rnd) as u64; total += (keccak_rc.num_rows() * n_keccak_rc) as u64; + total += (blake3.num_rows() * n_blake3) as u64; for t in eqs { total += (t.num_rows() * n_eq) as u64; } @@ -4357,6 +4599,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4375,6 +4618,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4468,6 +4712,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4482,6 +4727,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index d7969612f..73a567864 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -39,6 +39,9 @@ use crate::tables::bitwise::{ BitwiseOperation, BitwiseOperationType, bus_interactions as bitwise_bus_interactions, cols as bitwise_cols, }; +use crate::tables::blake3::{ + Blake3Constraints, bus_interactions as blake3_bus_interactions, cols as blake3_cols, +}; use crate::tables::branch::{ BranchConstraints, bus_interactions as branch_bus_interactions, cols as branch_cols, }; @@ -954,6 +957,18 @@ pub fn create_keccak_air(proof_options: &ProofOptions) -> ConcreteVmAir ConcreteVmAir { + build_air( + blake3_cols::NUM_COLUMNS, + blake3_bus_interactions(), + proof_options, + 1, + Blake3Constraints, + "BLAKE3", + ) +} + /// Create KECCAK_RND AIR with pi constraints and bus interactions. pub fn create_keccak_rnd_air(proof_options: &ProofOptions) -> ConcreteVmAir { build_air( diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index ffe9071b2..9c265d102 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1102,6 +1102,55 @@ fn test_prove_elfs_keccak_multi_call() { ); } +#[test] +fn test_prove_elfs_blake3() { + let _ = env_logger::builder().is_test(true).try_init(); + + let elf_bytes = crate::test_utils::asm_elf_bytes("test_blake3"); + let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); + let executor = + executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + let result = executor.run().expect("Failed to run program"); + + // The guest seeds the 14 input dwords with k+1, compresses, copies out over + // m and compresses again. Cross-check the committed output against a direct + // replay of the executor's compression function. + use executor::vm::instruction::execution::blake3_compress_6round; + let words: [u32; 28] = core::array::from_fn(|i| { + let dw = (i / 2 + 1) as u64; + if i % 2 == 0 { + dw as u32 + } else { + (dw >> 32) as u32 + } + }); + let h: [u32; 8] = words[0..8].try_into().unwrap(); + let m: [u32; 16] = words[8..24].try_into().unwrap(); + let t = (words[24] as u64) | ((words[25] as u64) << 32); + let (block_len, flags) = (words[26], words[27]); + let out1 = blake3_compress_6round(&h, &m, t, block_len, flags); + let out2 = blake3_compress_6round(&h, &out1, t, block_len, flags); + let expected_bytes: Vec = out2.iter().flat_map(|w| w.to_le_bytes()).collect(); + + assert_eq!( + result.return_values.memory_values, expected_bytes, + "committed output must match two chained 6-round compressions" + ); + + // Must use from_elf_and_logs (stack RAM needs PAGE tables, like keccak). + let mut traces = + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + assert_eq!( + traces.public_output_bytes, + result.return_values.memory_values + ); + + assert!( + prove_and_verify_vm_minimal(&elf, &mut traces), + "blake3 prove/verify failed" + ); +} + #[test] fn test_prove_elfs_ecsm() { let _ = env_logger::builder().is_test(true).try_init(); diff --git a/scripts/gen_blake3_bench.sh b/scripts/gen_blake3_bench.sh new file mode 100755 index 000000000..64d88bd17 --- /dev/null +++ b/scripts/gen_blake3_bench.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# +# gen_blake3_bench.sh — generate + compile a blake3-saturated guest. +# +# The guest seeds a 176-byte BLAKE3 state region (layout: h[4 dwords] | m[8] | +# t[1] | len,flags[1] | out[8]), then N times: fires the BLAKE3 6-round +# compression ecall and copies out over m, so every compression consumes the +# previous one's output — a strictly dependent chain that nothing can fold. +# Finally it commits the 64-byte output and halts. +# +# The BLAKE3 table commits ONE row per compression (fully unrolled layout), so +# padding-flush sweep points are simply powers of two: N = 2^k. Guest cost is +# ~21 cycles per compression (ecall + 8-dword copy + loop), so a 2^17-row table +# costs ~2.8M cycles — well inside a 2^22 epoch. +# +# ABI (executor/src/vm/instruction/execution.rs BLAKE3_SYSCALL_NUMBER): +# a7 = u64::MAX - 2, written as the sign-extended -3 +# a0 = 8-byte-aligned pointer to the 176-byte region +# +# Usage: scripts/gen_blake3_bench.sh N OUT.elf +# Honors CLANG / ASM_CFLAGS / ASM_LDFLAGS like the Makefile's asm rule. + +set -euo pipefail + +N="${1:?usage: gen_blake3_bench.sh N out.elf}" +OUT="${2:?usage: gen_blake3_bench.sh N out.elf}" + +if ! [[ "$N" =~ ^[0-9]+$ ]] || [ "$N" -lt 1 ]; then + echo "gen_blake3_bench.sh: N must be a positive integer, got '$N'" >&2 + exit 1 +fi + +CLANG="${CLANG:-clang}" +ASM_CFLAGS="${ASM_CFLAGS:---target=riscv64 -march=rv64im -mabi=lp64}" +ASM_LDFLAGS="${ASM_LDFLAGS:--fuse-ld=lld -nostdlib -Wl,-e,main}" + +if ! command -v "$CLANG" >/dev/null 2>&1; then + echo "gen_blake3_bench.sh: '$CLANG' not found; run 'make deps' or set CLANG=..." >&2 + exit 1 +fi + +SRC="$(mktemp "${TMPDIR:-/tmp}/blake3_bench.XXXXXX.s")" +trap 'rm -f "$SRC"' EXIT + +cat > "$SRC" < **A6R**: the BLAKE3 compression function restricted to 6 rounds is +> collision-resistant and suitable as a 2-to-1 compression for Merkle +> hashing and as a PRF for Fiat–Shamir, in the same sense the full 7-round +> function is believed to be (precedent: KangarooTwelve's reduced-round +> Keccak). + +Directed for implementation by the project owner, 2026-08-05 ("trust me" +sign-off in session; to be ratified in the spec before production use). From fb1afe800b9a25dfbdbc38f8323cff17a2f6207f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 5 Aug 2026 13:42:44 -0300 Subject: [PATCH 08/14] bench(blake3): drop the out->m copy chain from the bench guest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chaining cost an 8-dword copy loop = ~82 of ~87 guest cycles per compression (measured on the box), and those copy cycles become CPU/MEMW rows that dilute the very table being measured. The prover's cost per ecall is identical either way — the executor runs every call and nothing dedupes rows (timestamps differ) — so the loop body drops to ~5 cycles and the trace is >90% blake3-saturated, matching gen_keccak_bench.sh's shape. The e2e correctness test keeps its chained two-call form. --- scripts/gen_blake3_bench.sh | 39 ++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/scripts/gen_blake3_bench.sh b/scripts/gen_blake3_bench.sh index 64d88bd17..ce640739d 100755 --- a/scripts/gen_blake3_bench.sh +++ b/scripts/gen_blake3_bench.sh @@ -3,15 +3,22 @@ # gen_blake3_bench.sh — generate + compile a blake3-saturated guest. # # The guest seeds a 176-byte BLAKE3 state region (layout: h[4 dwords] | m[8] | -# t[1] | len,flags[1] | out[8]), then N times: fires the BLAKE3 6-round -# compression ecall and copies out over m, so every compression consumes the -# previous one's output — a strictly dependent chain that nothing can fold. -# Finally it commits the 64-byte output and halts. +# t[1] | len,flags[1] | out[8]), fires the BLAKE3 6-round compression ecall N +# times IN PLACE on that region, commits the 64-byte output and halts. +# +# The calls are deliberately NOT chained (out is not copied over m): chaining +# costs an 8-dword copy loop = ~82 of ~87 cycles per compression, and those +# copy cycles become CPU/MEMW rows that dilute the very table being measured. +# The prover's cost per ecall is identical either way — the executor runs every +# call, and no layer dedupes rows (timestamps differ per call) — so dropping +# the chain buys a ~5-cycle loop body and a >90% blake3-saturated trace, the +# same shape as gen_keccak_bench.sh. (The e2e correctness test, which is about +# values rather than cost, does chain: prover/src/tests test_prove_elfs_blake3.) # # The BLAKE3 table commits ONE row per compression (fully unrolled layout), so -# padding-flush sweep points are simply powers of two: N = 2^k. Guest cost is -# ~21 cycles per compression (ecall + 8-dword copy + loop), so a 2^17-row table -# costs ~2.8M cycles — well inside a 2^22 epoch. +# padding-flush sweep points are simply powers of two: N = 2^k. At ~5 +# cycles/compression a 2^17-row table costs ~0.7M cycles — inside one 2^20 +# epoch; use --epoch-size-log2 21 from N = 2^18 up. # # ABI (executor/src/vm/instruction/execution.rs BLAKE3_SYSCALL_NUMBER): # a7 = u64::MAX - 2, written as the sign-extended -3 @@ -62,27 +69,15 @@ main: li s0, $N .Lperm_loop: - # BLAKE3 6-round compression on the region. + # BLAKE3 6-round compression, in place. See the header for why the + # calls are independent rather than chained. mv a0, sp li a7, -3 ecall - # Chain: m <- out (8 dwords), so the next compression depends on this one. - li t1, 0 -.Lcopy_loop: - slli t2, t1, 3 - addi t3, sp, 112 - add t3, t3, t2 - ld t4, 0(t3) - addi t3, sp, 32 - add t3, t3, t2 - sd t4, 0(t3) - addi t1, t1, 1 - li t2, 8 - bne t1, t2, .Lcopy_loop addi s0, s0, -1 bnez s0, .Lperm_loop - # Commit the final 64-byte output so the chain is load-bearing. + # Commit the final 64-byte output so the work is load-bearing. li a0, 1 addi a1, sp, 112 li a2, 64 From 0f668f3c8b713902f5ac0ca7b55751a5f8b02401 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 5 Aug 2026 13:55:15 -0300 Subject: [PATCH 09/14] =?UTF-8?q?docs(blake3):=20poseidon2=20cost=20study?= =?UTF-8?q?=20=E2=80=94=20651=20cell-equiv/merge=20derived,=20calibrated?= =?UTF-8?q?=20to=20Plonky3=20exact=20and=20zisk=201%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-agent mining of the vendored references plus a synthesis under this repo's cost model (deg <=3 incl. mu-gating, LogUp aux 1.5 cells/send, the shipped syscall I/O idiom). Headline: ~651 table-only per 2-to-1 merge (in-place ABI variant), 8.2x under BLAKE3-6r table-only, ~5-9x end-to-end; the mu-gated degree cap alone doubles the permutation core vs ungated designs. Verdict: defensible number, but BLAKE3-6r already captures ~91% of the addressable per-merge saving vs keccak, and poseidon2 uniquely requires the verifier-hash switch, a GPU Merkle kernel, and a native-prover hashing slowdown. The gating measurement nobody has taken: hashing's share of a real recursion-verifier trace AFTER the blake3 switch. --- thoughts/blake3/poseidon2-cost-study.md | 123 ++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 thoughts/blake3/poseidon2-cost-study.md diff --git a/thoughts/blake3/poseidon2-cost-study.md b/thoughts/blake3/poseidon2-cost-study.md new file mode 100644 index 000000000..887902f11 --- /dev/null +++ b/thoughts/blake3/poseidon2-cost-study.md @@ -0,0 +1,123 @@ +# Poseidon2 accelerator — cost study vs BLAKE3-6r and keccak (2026-08-05) + +Produced by a multi-agent study (three mining agents over the vendored +references in `others/` — Plonky3, zisk, stwo, openvm, SP1 old+new, risc0, +airbender, pil2-proofman — plus a synthesis agent applying this repo's cost +model). Model calibration: reproduces Plonky3's Goldilocks w8/SR=1 column +count exactly (180) and zisk's measured 490 cells/perm to 1.0%. + +Companion measured numbers (this branch, 32-core box, blowup 2): +keccak-f 72,672 table / 73,020 end-to-end; BLAKE3-6r 5,316 table / 7,337 +end-to-end per 2-to-1 merge; blake3 throughput 5,217 compressions/s at 2^17 +rows vs keccak 433 perms/s at 2^20 rows. + +--- + +Both calibrations land: my model reproduces Plonky3's Goldilocks w8/SR=1 figure **exactly** (180), and at zisk's degree budget it gives 495 against zisk's measured 490 — **1.0%**. That's a two-point validation of the whole cost model before applying it to our constraints. + +--- + +# Poseidon2 accelerator chip — cost study (final) + +**Headline: ≈ 651 cell-equiv table-only per 2-to-1 merge** (recommended in-place ABI; 753 under the brief's separate-output ABI). Against BLAKE3-6r's 5,316 that is **8.2× cheaper**; against keccak-f's 72,672, **112×**. End-to-end the advantage over BLAKE3 holds at roughly 5–9×, but the absolute win is small change next to what BLAKE3 already banked. + +## Calibration first — the model reproduces two independent mined numbers + +Before trusting it on our constraints, I ran the same model at other systems' degree budgets: + +| target | their budget | my model | mined | agreement | +|---|---|---:|---:|---:| +| Plonky3 Goldilocks w8, `SBOX_REGISTERS=1`, lookup-free | deg 3, ungated | `8 + core(8,REG=1)` = **180** | 180 | **exact** | +| zisk Goldilocks perm, no sbox registers, incl. memory plumbing | deg 7, ungated | `187 + 86 + 24 + 198` = **495** | 490 | **1.0%** | + +The zisk check is the valuable one: it exercises the core formula, the byte-level I/O apparatus *and* the LogUp aux rate simultaneously, and lands within 1%. It also isolates the one thing that makes our number bigger than everyone else's — the degree budget, nothing else. + +## (b) Our number, line by line + +**Width 8, truncated permutation — justified.** A digest is 4 Goldilocks elements (32 B), so a 2-to-1 merge absorbs 8 elements. Two shapes do that in *one* permutation: width 8 as a truncated permutation (`P(left‖right)[0..4]` — Plonky3's `TruncatedPermutation`, `others/Plonky3/symmetric/src/compression.rs:17`), or width 12 as a rate-8/capacity-4 sponge (Plonky2 style). I priced both on identical I/O: **width 8 = 651, width 12 = 779**. Width 8 wins by 16% and is what Plonky3/SP1 ship for merges. Parameters are forced: `RF = 8 (4+4)`, `RP = 22`, S-box `x⁷` — `others/Plonky3/goldilocks/src/poseidon2.rs:22,32,70-73`; x³ and x⁵ are not permutations since `p−1 = 2^32·3·5·17·257·65537` (`goldilocks/src/poseidon1.rs:41-44`). + +**4 committed cells per S-box — forced by μ-gating, and minimal.** Max degree 3 *including* ×μ means bodies are capped at degree 2. Chain: `a=x²`, `b=a·x=x³`, `c=b·b=x⁶`, then `post = M·(c·x)` absorbs the last multiply into the linear layer. All four constraints are degree 2 → 3 after ×μ. Four is provably minimal: from `{1}`, three degree-≤2 steps reach at most exponent 6. This is Plonky3's `SBOX_REGISTERS=3` — their width formula (`poseidon2-air/src/columns.rs:12-69`) is generic in REGISTERS, but `eval_sbox` (`air.rs:288-323`) only ships `(7,1)→deg 3`, so we are one rung past anything in the wild. + +Committing the S-box *output* (Plonky3's `post_sbox`, `air.rs:274-277`) rather than the post-linear element (SP1's `s0`) keeps the whole state at expression-degree 1 through all 22 internal rounds, so the 7 non-S-boxed elements ride free and **no boundary re-commit is needed** — SP1's choice would cost +8 cells here. + +``` +CORE (one row per permutation, fully unrolled) + full rounds 2 × 4 rounds × 8 elems × (3 registers + 1 post) = 256 + partial rounds 22 rounds × 1 elem × (3 registers + 1) = 88 + core = 344 cells + sends in the core = 0 + — field-native: no ByteAlu, no AreBytes, no lookups whatsoever + cross-check: 8 inputs + 344 = 352 = Plonky3 num_cols<8,7,3,4,22> ✓ + +CANONICITY (byte→field must be injective or the tree isn't binding: + x and x+p are distinct byte strings with the same field element) + per element: commit is_max, dinv; constrain + μ·(is_max + (H−(2³²−1))·dinv − 1) deg 3 + μ·(is_max·(H−(2³²−1))) deg 3 + μ·(is_max·L) deg 3 booleanity implied + 2 cells, 0 sends × 12 elements = 24 cells + +I/O APPARATUS (idiom copied from the shipped chip, prover/src/tables/blake3.rs:97-123 + columns and :747-1030 interactions; 2 bytes per AreBytes send, 4 IsHalfword per + dword pointer, pointer-arith carries are expression-form with no cells) + + A: 12 dwords (brief) A′: 8 dwords, in-place (SP1 ABI) + TIMESTAMP_0/1 2 2 + ADDR bytes 8 8 + PTR halfwords 48 32 + IN bytes 64 64 + OUT bytes 32 32 + OLD_OUT bytes 32 0 ← old = the input bytes + MU 1 1 + I/O columns 187 139 + + Ecall receive 1 1 + Memw register read 1 1 + Memw per dword 12 8 + IsHalfword 48 32 + AreBytes addr 4 4 + ByteAlu AND (align) 1 1 + AreBytes IN/OUT/OLD 64 48 + sends N 131 95 + +TOTAL + A : main 187+344+24 = 555 ; aux = 1.5×131 = 198 ; TOTAL 753 + A′: main 139+344+24 = 507 ; aux = 1.5× 95 = 144 ; TOTAL 651 ← recommended +``` + +Arithmetic machine-checked: `/private/tmp/claude-501/-Users-maurofab-workspace-lambda-vm/931cf0e4-cfb3-4d8a-b940-5360f4374a8b/scratchpad/pos2_cost.py`. + +**End-to-end plumbing — the weakest number here, and I won't pretend otherwise.** The two known marginals disagree about what a memory op costs: BLAKE3 is `7,337 − 5,316 = 2,021` over 23 chip Memw ops (**88/op**); keccak is `73,020 − 72,672 = 348` over 26 (25 lanes + register read, `prover/src/tables/keccak.rs:3-5`) — **13/op**. A 6.5× spread means "per Memw op" is the wrong model. The likely driver is guest-side marshalling: BLAKE3's ABI makes the guest lay out a fresh 176-byte region every call, while keccak operates in place on a resident 200-byte state (hypothesis, unverified). Poseidon2-A′ is in-place over a 96-byte region with 9 ops, i.e. structurally keccak-shaped, so I expect the low end — but I quote the full band: + +``` +A′ end-to-end = 651 + 9 ops × [13 … 88] = [768 … 1,443] central estimate ≈ 900 +A end-to-end = 753 + 13 ops × [13 … 88] = [922 … 1,897] +``` + +## (c) Comparison, per 2-to-1 merge (64 B in, 32 B out) + +| | table-only | end-to-end | vs keccak (e2e) | vs BLAKE3-6r (e2e) | +|---|---:|---:|---:|---:| +| keccak-f (measured) | 72,672 | 73,020 | 1× | 0.10× | +| BLAKE3-6r (measured) | 5,316 | 7,337 | 10.0× | 1× | +| **Poseidon2 A** (derived) | **753** | ~922–1,897 (est) | 39–79× | 3.9–8.0× | +| **Poseidon2 A′** (derived, recommended) | **651** | ~768–1,443 (est) | 51–95× | 5.1–9.6× | +| Poseidon2 B (deg-4 bodies) | 479 | ~596–1,271 (est) | 57–123× | 5.8–12.3× | +| Poseidon2 C (internal bus, no memory) | 358 | 358 | 204× | 20.5× | + +Note the shape change between the two columns: table-only, Poseidon2 looks 112× better than keccak; end-to-end that collapses toward ~50–95×, because keccak's plumbing is rounding error against its enormous table while Poseidon2's plumbing is comparable to its entire chip. + +## (d) Caveats + +1. **The chip is I/O-bound, not hash-bound.** Core 344 cells; syscall apparatus 283 cell-equiv (139 cols + 144 aux) even in the in-place variant. Every lever that removes memory crossing beats every lever inside the permutation: in-place ABI −14%, internal Merkle-parent bus −45% (358). +2. **Canonical `< p` input range checks are required and cheap.** 2 cells + 3 constraints per element, 0 sends, 24 cells for all 12. SP1 Hypercube ships exactly this check on both inputs *and* outputs (`others/hypercube-verifier/crates/core/machine/src/operations/sp1_field_word.rs:44-88`; `input_range_checkers[16]` + `hash_result_range_checkers[16]` at `syscall/precompiles/poseidon2/air.rs:66-70`) — so it isn't optional in practice. Separately: absorbing *arbitrary* byte strings rather than chip-produced digests needs 7-byte-per-element packing to stay injective, cutting sponge rate 32 B → 28 B. +3. **The degree budget is the one thing making us expensive, and it's ours alone.** Every mined design runs ungated bodies. Our ×μ factor doubles the core (344 vs 172 at deg-3 bodies) and quadruples it against zisk's deg-7 budget (344 vs 86). Good news: `logup_max_degree` already floors any table with committed pairs at 3 (`crypto/stark/src/lookup.rs:2287-2298`), so degree 3 is free. Going to 4 costs one composition part for that table alone — `composition_poly_degree_bound = trace_length·(max_degree−1)` (`lookup.rs:1078`), i.e. 3 parts instead of 2 — in exchange for −172 cells/row. That trade is plausibly a win and should be measured, not assumed. +4. **The verifier hash must switch, and that is the real bill.** All three Merkle backends are keccak (`crypto/stark/src/config.rs:10,19,23`). A Poseidon2 chip pays for nothing unless FRI/Merkle/FS move to Poseidon2 — which means a new GPU Merkle kernel (the keccak one is at `crypto/stark/src/gpu_lde.rs:861`) and a native-prover slowdown of roughly 5–10× per byte versus keccak (**order-of-magnitude, unmeasured**). BLAKE3 is the opposite trade: faster than keccak natively, so switching costs the prover nothing. This asymmetry appears nowhere in the cell count and is the single biggest difference between the two candidates. +5. **Keccak is not displaceable either way.** EVM/ethrex needs keccak256. Poseidon2 and BLAKE3-6r compete for the same internal-hash slot. +6. **Constraint-eval cost ≠ cell cost.** The 22 internal rounds carry non-S-boxed state as symbolic linear combinations — degree stays 1 (that's the point) but fan-out reaches ~30 terms by the last round, ~700 extra field mults per row. Fine, provided the IR stays a DAG. +7. **Always-on AIR tax.** `FIXED_TABLE_COUNT` +1. Per the EC regression (PR #871: +3 near-empty AIRs → +25% prove time), a real-block ABBA is mandatory regardless of how good the cell count looks. +8. **Uncertainty.** Core 344 is exact given the design and validated to 1% against zisk. I/O is exact given the shipped idiom. Table-only band: **620–700 for A′**. End-to-end is the soft number, band **768–1,443**, and it is directly measurable rather than arguable. + +## (e) Verdict + +Poseidon2 beats BLAKE3-6r here, and by a solid margin: **8.2× table-only (651 vs 5,316), 5–9× end-to-end.** The derivation is well-anchored — the same model reproduces Plonky3's Goldilocks figure exactly and zisk's to 1% — so I'd defend the number itself. What I would not defend is the conclusion that this justifies building it. Measured against keccak end-to-end, BLAKE3-6r already captures **91%** of the total addressable saving per merge (65,683 of 72,120 cell-equiv); Poseidon2 adds the remaining 9%. And Poseidon2 cannot go much lower as a syscall — roughly half its cost is the ecall/MEMW apparatus it shares with every other chip, so even a perfect permutation would only reach ~400. Meanwhile it uniquely imposes a native-prover hashing slowdown and a new GPU Merkle kernel that BLAKE3 does not, and the in-VM digests it produces are field elements crossing a byte-addressed memory, which is what the canonicity gadget and the 64 AreBytes sends are paying for. The decision should turn on one measurement nobody has taken: after the BLAKE3 switch, what share of a real recursion-verifier trace is still hashing? That is precisely the question the EC campaign skipped — a −61.9% win on 0.61% of the trace — and it is cheap to answer before committing to a chip. If Poseidon2 is pursued anyway, the leverage order is unambiguous and none of it lives in the permutation: in-place ABI (−14%), internal Merkle-parent bus (−45%), then relaxing the μ-gated degree cap (−23%). \ No newline at end of file From e7b55fdf53428172e3f4b1283e261e5b2fa8e603 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 5 Aug 2026 14:27:51 -0300 Subject: [PATCH 10/14] =?UTF-8?q?feat(blake3):=20pin=20MU=20boolean=20loca?= =?UTF-8?q?lly=20=E2=80=94=20IS=5FBIT=20at=20idx=20813?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bus argument already pins mu to {0,1} indirectly (the Ecall receive anchors mu>0 rows to a CPU ecall whose ECALL flag is IS_BIT; MEMW's width flags are boolean), but that is a three-table argument written down nowhere. One ungated degree-2 constraint makes it local, matching ecsm/commit. Also corrects the constraint-layout doc: emission is grouped by op type across the row, not per G. --- prover/src/tables/blake3.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/prover/src/tables/blake3.rs b/prover/src/tables/blake3.rs index 4c08d5a5c..86b7f1f9f 100644 --- a/prover/src/tables/blake3.rs +++ b/prover/src/tables/blake3.rs @@ -1037,13 +1037,20 @@ pub fn bus_interactions() -> Vec { // Single-source constraint set // ========================================================================= -/// The BLAKE3 table's transition constraints: -/// - idx 0..44: 22 pointer `ADD` carry pairs (`ptr[k] = addr + 8k`, μ-gated); -/// - idx 44: μ·carry_1 = 0 — top-dword no-overflow (`addr + 168 = ptr[21]`); -/// - idx 45...: per G (16 each): two add3 groups (sum identity + 2 carry -/// booleanities), two add2 expression-carry booleanities, two rotations -/// (2 shift identities + 2 recombine identities each). All μ-gated, max -/// degree 3 (the booleanities; identities are degree 2). +/// The BLAKE3 table's transition constraints (814 total): +/// - idx 0..44: 22 pointer `ADD` carry pairs (`ptr[k] = addr + 8k`, μ-gated); +/// - idx 44: μ·carry_1 = 0 — top-dword no-overflow (`addr + 168 = ptr[21]`); +/// - idx 45..333: all 96 add3 groups (sum identity + 2 carry booleanities); +/// - idx 333..429: all 96 add2 expression-carry booleanities; +/// - idx 429..813: all 96 rotations (2 shift identities + 2 recombine each). +/// NOTE the grouping is by op type across the whole row, NOT per G — G #g's +/// 16 constraints are scattered across the three bands. +/// - idx 813: `IS_BIT(MU)` — μ·(1−μ) = 0, ungated. The bus argument pins +/// μ to {0,1} indirectly (the Ecall receive anchors μ>0 rows to a CPU ecall +/// whose ECALL flag is IS_BIT; MEMW's width flags are boolean), but that is +/// an inter-table argument — this makes it local, matching ecsm/commit. +/// +/// All μ-gated, max degree 3 (the booleanities; identities are degree 2). #[derive(Clone, Copy)] pub struct Blake3Constraints; @@ -1196,6 +1203,11 @@ impl ConstraintSet for Blake3Constraints { b.emit_base(idx, m * (yhi - sll_lo - sllc_hi)); idx += 1; } + + // idx 813: IS_BIT(MU) — ungated booleanity, degree 2. See the struct + // doc for why this is emitted even though the bus argument already + // pins μ indirectly. + crate::constraints::templates::emit_is_bit(b, idx, cols::MU, None); } } From 2e0f0b4158346fa6d60ffa254cec06f86a607479 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 6 Aug 2026 13:04:25 -0300 Subject: [PATCH 11/14] spec(blake3): chip page, machine-checked I/O surface, A6R assumption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the BLAKE3_6R accelerator to the spec book. spec/src/blake3.toml machine-formalizes the chip's I/O-and-range surface: the ECALL binding, the x10 register read, all 22 MEMW dword operations, the pointer arithmetic and every explicit ARE_BYTES/IS_HALF/IS_BIT check — exactly the surface the z3 gate does not model (DESIGN.md §1.1, §7 items 4/5/10). The spec machinery derives 565 interactions from it. That number cross-checks the implementation: 565 + the 832 BYTE_ALU[XOR] lookups of the unrolled mixing core = 1,397 = the chip's exact interaction count, and the toml's column tally matches NUM_COLUMNS = 3,219. The mixing core is deliberately not re-formalized here; its normative sources are the single-source Rust dataflow (run_flow in prover/src/tables/blake3.rs, interpreted once as columns and once as witness) and the z3 gate that proves that dataflow equal to the reference function. spec/blake3.typ states that scope split explicitly rather than implying whole-chip coverage. The page also records A6R as a named assumption: 6-round BLAKE3 is collision-resistant and PRF-suitable in the same sense the 7-round function is believed to be (K12 precedent). The gate proves the chip computes 6-round BLAKE3 correctly; it neither proves nor addresses whether 6 rounds are secure, and any Merkle/Fiat-Shamir use invokes the assumption. The assumption-free alternative is priced next to it: the design is round-parameterised, and a 7-round instantiation — bit-compatible with official BLAKE3 parent merges — costs roughly 10-12% more per merge and needs no novel assumption. Registers the two missing ecall numbers in about_ecalls.typ: -3 (BLAKE3_6R, u64::MAX-2) and -11 (ECSM, u64::MAX-10). thoughts/blake3/README.md gets a truth pass — it claimed no Rust chip existed and that the transcription audit was still owed; both are stale as of PR #903. --- spec/about_ecalls.typ | 7 +- spec/blake3.typ | 153 +++++++++++++++++ spec/book.typ | 1 + spec/src/blake3.toml | 345 ++++++++++++++++++++++++++++++++++++++ thoughts/blake3/README.md | 24 ++- 5 files changed, 526 insertions(+), 4 deletions(-) create mode 100644 spec/blake3.typ create mode 100644 spec/src/blake3.toml diff --git a/spec/about_ecalls.typ b/spec/about_ecalls.typ index 9b37d5f21..1e655e0a9 100644 --- a/spec/about_ecalls.typ +++ b/spec/about_ecalls.typ @@ -31,4 +31,9 @@ Negative numbers (represented as 2s complement 64-bit numbers), are used for our / 64: `write` (@commit) / 93: `exit` (@halt) / -1: `SHA256` (@sha256) -/ -2: `KECCAK` (@keccak) \ No newline at end of file +/ -2: `KECCAK` (@keccak) +/ -3: `BLAKE3_6R` (@blake3) — the *6-round internal variant* of the BLAKE3 + compression function, not standard BLAKE3; see the named assumption on its + page. +/ -11: `ECSM` — secp256k1 scalar multiplication (implemented in + `executor/src/vm/instruction/execution.rs`; spec page pending). \ No newline at end of file diff --git a/spec/blake3.typ b/spec/blake3.typ new file mode 100644 index 000000000..2cac4ba57 --- /dev/null +++ b/spec/blake3.typ @@ -0,0 +1,153 @@ +#import "/book.typ": book-page, aside +#import "/src.typ": load_config, load_chip +#import "/chip.typ": ( + compute_nr_interactions, + render_chip_assumptions, + render_chip_variable_table, + total_nr_variables, + total_nr_instantiated_columns, + render_constraint_table, + render_chip_padding_table, +) + +#let config = load_config() +#let chip = load_chip("src/blake3.toml", config) + +#show: book-page(chip.name) +#let blake3 = raw(chip.name) + +The #blake3 chip applies the *6-round internal variant* of the BLAKE3 +compression function to a 176-byte memory region. It is an internal +Merkle / Fiat–Shamir hash accelerator: one compression digests a 64-byte +message block — exactly one 2-to-1 merge of two 32-byte chaining values — +and produces the full 16-word output (the truncated chaining value is +`out[0..8]`). + +⚠ *This is not standard BLAKE3.* The standard function applies 7 rounds; +this chip applies 6 (see @blake3-a6r). No external system will ever agree +on these digests. Standard library implementations (e.g. the official +`blake3` Rust crate) hardwire 7 rounds and cannot compute this function; +the host-side reference implementation lives in +`executor/src/vm/instruction/execution.rs` (`blake3_compress_6round`), +differentially pinned to the validated oracle in +`thoughts/blake3/blake3-oracle/`. + += ECALL interface + +ECALL number `-3` (`0xFFFF_FFFF_FFFF_FFFD`). `A0` holds an 8-byte-aligned +pointer to a 176-byte state region of 22 consecutive little-endian dwords: + +#table( + columns: (auto, auto, auto), + [*dwords*], [*contents*], [*direction*], + [0..=3], [`h[0..8]` chaining value (2 u32 words per dword)], [read], + [4..=11], [`m[0..16]` message block], [read], + [12], [`t` counter (`t_lo` = low u32 → `v[12]`, `t_hi` = high u32 → `v[13]`)], [read], + [13], [`block_len` (low u32) | `flags` (high u32)], [read], + [14..=21], [`out[0..16]`], [written], +) + +Unaligned or overflowing state addresses are rejected by the executor. +The counter split order (`t_lo → v[12]`, `t_hi → v[13]`) is load-bearing +and was behaviourally verified against the official BLAKE3 crate through +two independent counter paths (oracle audit, 44/44). + += Chip structure + +== Columns and interactions + +#let nr_variables = total_nr_variables(chip) +#let nr_columns = total_nr_instantiated_columns(chip, config) +#let nr_interactions = compute_nr_interactions(chip) + +The formalized I/O-and-range surface below comprises #nr_variables +variables over #nr_columns columns and #nr_interactions interactions; the +full chip has 3,219 main columns and 1,397 interactions (the difference is +the mixing core's `BYTE_ALU[XOR]` lookups, whose SSA operand wiring is +normatively specified by the single-source Rust dataflow and the z3 gate — +see @blake3-scope). + +#render_chip_variable_table(chip, config) + +== Structure + +One row per compression call, fully unrolled: 6 rounds × 8 G-functions in +SSA form. The message schedule is a compile-time index permutation (the +`sched` array in `run_flow`, composed from `BLAKE3_MSG_PERMUTATION`), so +every round references the 16 original committed message words — there is +no state or message handoff between rows. I/O follows the KECCAK core idiom: an `ECALL` receiver binds +(timestamp, syscall number), a `MEMW` register read binds the `x10` +pointer, and 22 per-dword `MEMW` operations carry the reads and writes. + +Key constraint-design decisions (full rationale: +`thoughts/blake3/blake3-chip/DESIGN.md`, deltas in `IMPLEMENTATION.md`): + +- every eval constraint is gated by the multiplicity column $mu$, and the + maximum constraint degree *including* the $times mu$ factor is 3; +- 3-operand adds commit *two summed carry bits* with an explicit sum + identity (a ternary carry would be degree 4 after gating); +- 2-operand adds use an expression carry (no committed cell) with a + $mu$-gated booleanity; +- `rotr16`/`rotr8` are free byte relabels; `rotr12`/`rotr7` are inline + $mu$-gated Euclidean shift identities whose soundness rests on the + tight $[0, 2^16)$ bound of the `SLL` halfwords ($2^16$ is invertible + mod $p$); +- every add/shift output feeds a downstream `BYTE_ALU[XOR]` lookup, which + is its only byte range check; the message words, the previous + out-region content and the address bytes are never XOR-consumed and + carry explicit `ARE_BYTES` checks instead. + +The chip's wiring is single-sourced: the compression dataflow is written +once in `prover/src/tables/blake3.rs` (`run_flow`) and interpreted both as +column wiring (constraints + bus senders) and as the u32 witness (trace +fill + lookup multiplicities), so the two cannot diverge structurally. + +== Formalized constraints + +#render_constraint_table(chip, config, groups: "io") +#render_constraint_table(chip, config, groups: "addr") +#render_constraint_table(chip, config, groups: "range") +#render_constraint_table(chip, config, groups: "mu") + += Verification evidence + +The design was taken to a z3-gated model *before* the Rust implementation +(`thoughts/blake3/blake3-chip/z3_blake_verify.py`): the G quarter-round +and the init/feed-forward layout are UNSAT under free inputs, five +negative controls and two field-level bound-necessity controls are SAT, +and the concrete 6- and 7-round pipelines reproduce the oracle's pinned +vectors. Two independent transcription audits +(`thoughts/blake3/TRANSCRIPTION-AUDIT.md`, +`GATE-TRANSCRIPTION-AUDIT.md`) checked the gate against the oracle. The +Rust chip is additionally pinned by the 10 canonical 6-round vectors at +the syscall level and by an end-to-end prove+verify of chained +compressions. + += The 6-round assumption + +*A6R.* The BLAKE3 compression function restricted to 6 rounds is +collision-resistant and suitable as a 2-to-1 compression for Merkle +hashing and as a PRF for Fiat–Shamir, in the same sense the full 7-round +function is believed to be. (Precedent: KangarooTwelve's reduced-round +Keccak. Best public cryptanalysis of BLAKE3 reaches far fewer rounds; the +margin removed here is one round of seven.) + +Any use of #blake3 as a Merkle or transcript hash *invokes this +assumption*. The z3 gate proves the chip computes 6-round BLAKE3 +correctly; it neither proves nor addresses whether 6 rounds are secure. + +*The assumption-free alternative it is priced against.* The chip design +is round-parameterised; a 7-round instantiation (standard BLAKE3 +compression, bit-compatible with official parent-node merges) costs +roughly 10–12% more per merge end-to-end and requires no novel +assumption. If that margin is not needed, prefer the 7-round variant; if +both are instantiated they are distinct chips with distinct ECALL +numbers. + += Cost + +Measured on the CPU bench box (32 cores, blowup 2, single-epoch +continuations): ≈5,473 compressions/s at ≥#raw("2^17") table rows, ≈7,194 +committed cell-equivalents per compression end-to-end (≈5,316 table-only) +— ≈12× the keccak-f permutation per 2-to-1 merge at equal wall time and +memory. Details and methodology: PR \#903. diff --git a/spec/book.typ b/spec/book.typ index 8bf8612af..7faf8ef1b 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -49,6 +49,7 @@ ("commit.typ", [`COMMIT` chip], ), ("sha256.typ", [`SHA256` accelerator], ), ("keccak.typ", [`KECCAK` accelerator], ), + ("blake3.typ", [`BLAKE3_6R` accelerator], ), )) ) ) diff --git a/spec/src/blake3.toml b/spec/src/blake3.toml new file mode 100644 index 000000000..bb04388c0 --- /dev/null +++ b/spec/src/blake3.toml @@ -0,0 +1,345 @@ +# BLAKE3_6R — the 6-round internal-variant BLAKE3 compression accelerator. +# +# ⚠ NORMATIVE SOURCE NOTE. This spec documents the shipped chip +# (`prover/src/tables/blake3.rs`); the chip's wiring is single-sourced in Rust +# (`run_flow` interpreted as columns and as witness) and formally gated by the +# z3 model in `thoughts/blake3/blake3-chip/z3_blake_verify.py`. Where this file +# and those artifacts disagree, THEY are normative and this file has a bug. +# Cross-checked totals at spec-writing time: 3,219 main columns, 1,397 +# interactions, 814 constraints, max degree 3 (incl. the ×μ gating factor). +# +# One row = one compression call: 6 rounds × 8 G-functions fully unrolled in +# SSA form. Message schedule = the literal per-round index table `SCHED` below +# (permute^r of the identity under MSG_PERMUTATION = +# [2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]); round r position i consumes +# original message word SCHED[r][i]. +# +# Security: this chip computes 6-round BLAKE3, NOT the standard 7-round +# function. Its use as a Merkle / Fiat–Shamir hash rests on the named +# assumption A6R (see blake3.typ). No external system will ever agree on +# these digests. + +name = "BLAKE3" + +# ------------------------------------------------------------------------- +# Inputs (read from memory at addr .. addr+112; see the ECALL ABI) +# ------------------------------------------------------------------------- + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "timestamp at which the compression is performed" +pad = 0 + +[[variables.input]] +name = "addr" +type = "DWordBL" +desc = "8-aligned base address of the 176-byte state region (h|m|t|len,flags|out)" +pad = 0 + +[[variables.input]] +name = "h" +type = [["Byte", 4], 8] +desc = "input chaining value h[0..8], 8 little-endian u32 words" +pad = 0 + +[[variables.input]] +name = "m" +type = [["Byte", 4], 16] +desc = "message block m[0..16] = left_cv ‖ right_cv for a 2-to-1 merge" +pad = 0 + +[[variables.input]] +name = "t_lo" +type = ["Byte", 4] +desc = "low u32 of the 64-bit counter t → v[12]" +pad = 0 + +[[variables.input]] +name = "t_hi" +type = ["Byte", 4] +desc = "high u32 of the 64-bit counter t → v[13] (split order is load-bearing)" +pad = 0 + +[[variables.input]] +name = "block_len" +type = ["Byte", 4] +desc = "input byte count of this block → v[14]" +pad = 0 + +[[variables.input]] +name = "flags" +type = ["Byte", 4] +desc = "domain-separation flags → v[15]" +pad = 0 + +# ------------------------------------------------------------------------- +# Outputs (written to memory at addr+112 .. addr+176) +# ------------------------------------------------------------------------- + +[[variables.output]] +name = "out" +type = [["Byte", 4], 16] +desc = "full 16-word compression output; the truncated CV is out[0..8]" +pad = 0 + +# ------------------------------------------------------------------------- +# Auxiliary +# ------------------------------------------------------------------------- + +[[variables.auxiliary]] +name = "state_ptr" +type = ["DWordHL", 22] +desc = "per-dword pointers state_ptr[k] = addr + 8k over the 22-dword region" +pad = ["*", 8, ["arr", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]] + +[[variables.auxiliary]] +name = "old_out" +type = [["Byte", 8], 8] +desc = "previous memory content of the out region, dword o = bytes old_out[o]; the MEMW writes' old value" +pad = 0 + +# Per-G SSA cells, 48 instances (g = 8r + j, round r ∈ 0..6, call j ∈ 0..8). +# Each G block: A1(4B) c1,c2(2 bits) X1(4B) C1(4B) X2(4B) +# R1: SLL_lo(2B) SLLC_lo(2B) SLL_hi(2B) SLLC_hi(2B) Y1(4B) +# A2(4B) c3,c4(2 bits) X3(4B) C2(4B) X4(4B) +# R2: same shape as R1 +# = 56 byte cells + 4 carry bits = 60 cells; column base = 210 + 60g +# (see prover/src/tables/blake3.rs `cols` for the exact offsets). + +[[variables.auxiliary]] +name = "g_add3_out" +type = [[["Byte", 4], 2], 48] +desc = "A1, A2 per G: the two 3-operand add outputs (v[a] after each half)" +pad = 0 + +[[variables.auxiliary]] +name = "g_add3_carry" +type = [[["Bit", 2], 2], 48] +desc = "two summed carry bits per 3-operand add: carry = c1 + c2 ∈ {0,1,2}" +pad = 0 + +[[variables.auxiliary]] +name = "g_xor_out" +type = [[["Byte", 4], 4], 48] +desc = "X1..X4 per G: the four 32-bit XOR outputs (rotr16/rotr8 are free byte relabels of X1/X3)" +pad = 0 + +[[variables.auxiliary]] +name = "g_add2_out" +type = [[["Byte", 4], 2], 48] +desc = "C1, C2 per G: the two 2-operand add outputs (v[c]); carries are expressions, not cells" +pad = 0 + +[[variables.auxiliary]] +name = "g_rot" +type = [[["Byte", 12], 2], 48] +desc = "per rotation (rotr12 then rotr7): SLL_lo, SLLC_lo, SLL_hi, SLLC_hi (2 B each) and the output word Y (4 B)" +pad = 0 + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "1 on real rows, 0 on padding; gates every constraint and interaction; pinned boolean by an ungated IS_BIT" +pad = 0 + +# ------------------------------------------------------------------------- +# Constants +# ------------------------------------------------------------------------- + +[[constants]] +name = "IV" +desc = "BLAKE3 IV[0..4] (SHA-256 IV words), inlined into round-0 arithmetic — not columns" +value = ["arr", 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A] + +[[constants]] +name = "SCHED" +desc = "per-round message-schedule index table: round r position i reads m[SCHED[r][i]]; SCHED[r] = permute^r(identity), MSG_PERMUTATION = [2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]; the trailing permute after round 5 is never consumed" +value = ["arr", + ["arr", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + ["arr", 2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8], + ["arr", 3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1], + ["arr", 10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6], + ["arr", 12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4], + ["arr", 9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7], +] + +[[constants]] +name = "G_INDICES" +desc = "the (a,b,c,d) working-state slots of the 8 G-calls per round: 4 column mixes then 4 diagonal mixes" +value = ["arr", + ["arr", 0, 4, 8, 12], ["arr", 1, 5, 9, 13], ["arr", 2, 6, 10, 14], ["arr", 3, 7, 11, 15], + ["arr", 0, 5, 10, 15], ["arr", 1, 6, 11, 12], ["arr", 2, 7, 8, 13], ["arr", 3, 4, 9, 14], +] + +# ------------------------------------------------------------------------- +# Constraint groups. All eval constraints are μ-gated; padding rows are +# all-zero (except the keccak-idiom state_ptr pad) with μ = 0. +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +# Constraint groups. +# +# ⚠ SCOPE. This file machine-formalizes the chip's I/O AND RANGE surface — +# the ECALL binding, the x10 register read, all 22 MEMW dword operations, +# the pointer arithmetic, and every explicit ARE_BYTES/IS_HALF/IS_BIT — i.e. +# exactly the surface the z3 gate does NOT model (DESIGN.md §1.1: "the gate +# cannot check this", §7 items 4/5/10). The unrolled 6-round MIXING CORE +# (96 add3 sum identities + carry booleanities, 96 add2 expression-carry +# booleanities, 96 inline rotation identity groups, 832 BYTE_ALU[XOR] +# lookups wired per the SSA dataflow) is NOT re-formalized here: its +# normative sources are the single-source Rust dataflow +# (prover/src/tables/blake3.rs `run_flow`, interpreted once as columns and +# once as witness) and the z3 gate that proves that dataflow equal to the +# reference function under free inputs. Totals for cross-checking: 814 +# constraints, 1,397 interactions, max degree 3 including ×μ. +# ------------------------------------------------------------------------- + +[[constraint_groups]] +name = "io" + +# ECALL receive: [timestamp, -3 as u64 = 2^64 - 3] +[[constraints.io]] +kind = "interaction" +tag = "ECALL" +input = ["timestamp", ["cast", ["-", ["^", 2, 64], 3], "DWordWL"]] +multiplicity = ["-", "μ"] + +# MEMW register read of x10 binding addr (keccak:c:read_addr idiom) +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 10], "DWordWL"], "addr", "timestamp", 1, 0, 0] +output = "addr" +multiplicity = "μ" +ref = "blake3:c:read_addr" + +# The 22 dword MEMW operations at `timestamp`, per region. Input dwords +# (k < 14) are reads in the combined read+write encoding: old = value = the +# input words, timestamps advance. Output dwords (k >= 14) write `out` over +# `old_out`. + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", "k"], "DWordWL"], ["idx", "h", ["*", 2, "k"]], ["idx", "h", ["+", ["*", 2, "k"], 1]], "timestamp", 0, 0, 1] +output = ["arr", ["idx", "h", ["*", 2, "k"]], ["idx", "h", ["+", ["*", 2, "k"], 1]]] +iters = [["k", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:load_h" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", ["+", "k", 4]], "DWordWL"], ["idx", "m", ["*", 2, "k"]], ["idx", "m", ["+", ["*", 2, "k"], 1]], "timestamp", 0, 0, 1] +output = ["arr", ["idx", "m", ["*", 2, "k"]], ["idx", "m", ["+", ["*", 2, "k"], 1]]] +iters = [["k", 0, 7]] +multiplicity = "μ" +ref = "blake3:c:load_m" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", 12], "DWordWL"], "t_lo", "t_hi", "timestamp", 0, 0, 1] +output = ["arr", "t_lo", "t_hi"] +multiplicity = "μ" +ref = "blake3:c:load_t" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", 13], "DWordWL"], "block_len", "flags", "timestamp", 0, 0, 1] +output = ["arr", "block_len", "flags"] +multiplicity = "μ" +ref = "blake3:c:load_len_flags" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", ["+", "o", 14]], "DWordWL"], ["idx", "out", ["*", 2, "o"]], ["idx", "out", ["+", ["*", 2, "o"], 1]], "timestamp", 0, 0, 1] +output = ["idx", "old_out", "o"] +iters = [["o", 0, 7]] +multiplicity = "μ" +ref = "blake3:c:store_out" + +[[constraint_groups]] +name = "addr" + +# state_ptr[k] = addr + 8k via the shared ADD template (μ-gated carry pair); +# the top dword k = 21 additionally forbids wraparound (addr + 168 < 2^64). +[[constraints.addr]] +kind = "template" +tag = "ADD" +input = [["cast", "addr", "DWordWL"], ["cast", ["*", 8, "k"], "DWordWL"]] +output = ["cast", ["idx", "state_ptr", "k"], "DWordWL"] +iters = [["k", 0, 21]] +ref = "blake3:c:state_ptr" + +# alignment: addr[0] & 7 = 0 +[[constraints.addr]] +kind = "interaction" +tag = "BYTE_ALU" +input = [0, ["idx", "addr", 0], 7, 0] +multiplicity = "μ" +ref = "blake3:c:alignment" + +# addr byte range checks — without them the addr_lo/addr_hi linear +# combinations alias non-byte encodings (keccak:c:range_addr rationale). +[[constraints.addr]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", "addr", ["*", 2, "i"]], ["idx", "addr", ["+", ["*", 2, "i"], 1]]] +iters = [["i", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_addr" + +[[constraints.addr]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", ["cast", ["idx", "state_ptr", "k"], "DWordHL"], "hw"]] +iters = [["k", 0, 21], ["hw", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_state_ptr" + +[[constraint_groups]] +name = "range" + +# m is never XOR-consumed — explicit byte checks (DESIGN §4.7 / §7.5). +[[constraints.range]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", ["idx", "m", "i"], ["*", 2, "p"]], ["idx", ["idx", "m", "i"], ["+", ["*", 2, "p"], 1]]] +iters = [["i", 0, 15], ["p", 0, 1]] +multiplicity = "μ" +ref = "blake3:c:range_m" + +# old_out rides only the MEMW bus — same aliasing argument as the address. +[[constraints.range]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", ["idx", "old_out", "o"], ["*", 2, "p"]], ["idx", ["idx", "old_out", "o"], ["+", ["*", 2, "p"], 1]]] +iters = [["o", 0, 7], ["p", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_old_out" + +# The four shift halfwords of each inline rotation (SLL_lo, SLLC_lo, +# SLL_hi, SLLC_hi — bytes 0..8 of each rotation block). The tight SLL +# bounds are load-bearing for the rotation identities (DESIGN §4.2). +[[constraints.range]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", ["idx", ["idx", "g_rot", "g"], "half"], ["*", 2, "p"]], ["idx", ["idx", ["idx", "g_rot", "g"], "half"], ["+", ["*", 2, "p"], 1]]] +iters = [["g", 0, 47], ["half", 0, 1], ["p", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_rot" + +[[constraint_groups]] +name = "mu" + +# μ boolean, ungated — the ECALL receive anchors μ>0 rows to a CPU ecall +# whose ECALL flag is boolean; this constraint makes the argument local. +[[constraints.mu]] +kind = "template" +tag = "IS_BIT" +input = ["μ"] +ref = "blake3:c:range_mu" diff --git a/thoughts/blake3/README.md b/thoughts/blake3/README.md index 7a30680c7..1892bbf47 100644 --- a/thoughts/blake3/README.md +++ b/thoughts/blake3/README.md @@ -14,9 +14,14 @@ the repo. ## What this is -A BLAKE3 compression-function accelerator taken to a **gate-proved design** and -stopped there, blocked on a protocol decision — sign-off on a named "6-round -collision resistance" assumption. No Rust chip was ever written. +A BLAKE3 compression-function accelerator taken to a **gate-proved design**, +and — as of 2026-08-05 — **implemented in Rust** (PR #903: executor syscall +`u64::MAX-2`, chip `prover/src/tables/blake3.rs`, adversarially reviewed, e2e +prove+verify green, measured 12.2× keccak merges/s). The named "6-round +collision resistance" assumption (A6R) is recorded in the spec +(`spec/blake3.typ`) and in `blake3-chip/IMPLEMENTATION.md`; production use as +a Merkle/FS hash still requires ratifying it (or shipping the assumption-free +7-round instantiation, which costs ~10-12% more per merge). Purpose is **internal** (Merkle / Fiat–Shamir replacement candidate; the 6-round variant is the primary target, K12 as precedent). The EVM has no BLAKE3 — only @@ -33,7 +38,12 @@ that stays guest code. | `blake3-oracle/official_test_vectors.json` | 35-case vector set — **see provenance note below** | | `blake3-oracle/canonical_6round_vectors.json` | 10 pinned 6-round vectors, regenerated by the harness | | `blake3-chip/DESIGN.md` | chip design + §7 risk ledger | +| `blake3-chip/IMPLEMENTATION.md` | Rust-implementation notes: deltas from the design + gates run | | `blake3-chip/z3_blake_verify.py` | the soundness gate | +| `TRANSCRIPTION-AUDIT.md` | audit: oracle → gate transcription | +| `GATE-TRANSCRIPTION-AUDIT.md` | audit: gate constraint-model transcription | +| `audit_gate_transcription.py` | the executable half of the gate audit | +| `poseidon2-cost-study.md` | poseidon2-vs-blake3 cost study (2026-08-05) | | `ground-truth/` | tiny Rust generator that produced the vector set from the official `blake3` crate | ## Re-validation, 2026-07-29 — everything runs and passes @@ -165,6 +175,11 @@ official reference implementation using the official vector parameters". ## Still unaudited — where to send the next reviewer +*(2026-08-05 update: the transcription audit this section asks for has since +been done — see `TRANSCRIPTION-AUDIT.md` and `GATE-TRANSCRIPTION-AUDIT.md`, +which found and fixed the issues recorded in DESIGN.md §4.2/§7. The section is +kept for its account of WHY that audit mattered.)* + Two independent reviews established that **the oracle defines the right function**, so the gate's UNSATs are about the right function. They did *not* audit the step after that: **nobody has checked the z3 gate's transcription of @@ -187,6 +202,9 @@ v1.0.9) remain unreproduced as recorded. ## If this is picked up again +*(2026-08-05: it was picked up — see the top of this file. The A6R assumption +is now written down in `spec/blake3.typ`; formal ratification remains open.)* + The blocking item is a **protocol decision, not an engineering one**: the gate proves the chip matches the reference, *not* that 6 rounds are secure. That needs a named, signed assumption in the spec. From a7a8bdd5c89a669c713f732a8f670afb761ede69 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 6 Aug 2026 13:05:12 -0300 Subject: [PATCH 12/14] spec(blake3): record the external expert review of the 6-round margin Relayed by the project owner: external symmetric-cryptography experts judged removing one round (7->6) comfortable and removing two (7->5) explicitly not. The spec's A6R section now records this, fixes 6 rounds as the endorsed floor (sub-6 variants MUST NOT be instantiated), and reframes 7R as the interop/zero-assumption fallback rather than the preferred default. --- spec/blake3.typ | 20 ++++++++++++------- thoughts/blake3/blake3-chip/IMPLEMENTATION.md | 9 ++++++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spec/blake3.typ b/spec/blake3.typ index 2cac4ba57..0d471378d 100644 --- a/spec/blake3.typ +++ b/spec/blake3.typ @@ -132,17 +132,23 @@ function is believed to be. (Precedent: KangarooTwelve's reduced-round Keccak. Best public cryptanalysis of BLAKE3 reaches far fewer rounds; the margin removed here is one round of seven.) +*External review (2026-08).* The round-count choice was reviewed with +external symmetric-cryptography experts consulted by the project: removing +*one* round (7 → 6) was judged comfortable; removing *two* (7 → 5) was +explicitly not. Accordingly, 6 rounds is the endorsed floor — variants +below 6 rounds are out of scope and MUST NOT be instantiated. + Any use of #blake3 as a Merkle or transcript hash *invokes this assumption*. The z3 gate proves the chip computes 6-round BLAKE3 correctly; it neither proves nor addresses whether 6 rounds are secure. -*The assumption-free alternative it is priced against.* The chip design -is round-parameterised; a 7-round instantiation (standard BLAKE3 -compression, bit-compatible with official parent-node merges) costs -roughly 10–12% more per merge end-to-end and requires no novel -assumption. If that margin is not needed, prefer the 7-round variant; if -both are instantiated they are distinct chips with distinct ECALL -numbers. +*The assumption-free alternative.* The chip design is round-parameterised; +a 7-round instantiation (standard BLAKE3 compression, bit-compatible with +official parent-node merges) costs roughly 10–12% more per merge +end-to-end and requires no assumption beyond standard BLAKE3. The 6-round +variant is the primary internal target per the review above; the 7-round +variant is the interoperability / zero-assumption fallback. If both are +instantiated they are distinct chips with distinct ECALL numbers. = Cost diff --git a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md index 72c330819..993b0bda3 100644 --- a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md +++ b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md @@ -92,4 +92,11 @@ assumption: > Keccak). Directed for implementation by the project owner, 2026-08-05 ("trust me" -sign-off in session; to be ratified in the spec before production use). +sign-off in session). Recorded in the spec (`spec/blake3.typ`, A6R section). + +**External review (2026-08-06, relayed by the project owner):** the round +count was reviewed with external symmetric-cryptography experts — removing +one round (7→6) judged comfortable, removing two (7→5) explicitly not. +6 rounds is therefore the endorsed floor; sub-6-round variants are +permanently out of scope. The 7-round instantiation remains available as +the zero-assumption / interop fallback at ~10-12% more per merge. From 783c5a951439113e62709f3c81049f18ebe9dfbd Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 6 Aug 2026 13:06:21 -0300 Subject: [PATCH 13/14] spec(blake3): sub-6 rounds is expert-gated, not forbidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wording fix per the project owner: variants below 6 rounds are not formally ruled out — they are unavailable on the project's own authority and would require dedicated external cryptanalytic review of the reduced margin, rather than being an engineering or configuration decision. --- spec/blake3.typ | 7 +++++-- thoughts/blake3/blake3-chip/IMPLEMENTATION.md | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/blake3.typ b/spec/blake3.typ index 0d471378d..939e71cda 100644 --- a/spec/blake3.typ +++ b/spec/blake3.typ @@ -135,8 +135,11 @@ margin removed here is one round of seven.) *External review (2026-08).* The round-count choice was reviewed with external symmetric-cryptography experts consulted by the project: removing *one* round (7 → 6) was judged comfortable; removing *two* (7 → 5) was -explicitly not. Accordingly, 6 rounds is the endorsed floor — variants -below 6 rounds are out of scope and MUST NOT be instantiated. +explicitly not. Accordingly, 6 rounds is the endorsed floor. Variants +below 6 rounds are not formally ruled out, but they are not available on +the project's own authority: adopting one would require the external +experts to study the reduced-round margin specifically — a dedicated +cryptanalytic review, not an engineering or configuration decision. Any use of #blake3 as a Merkle or transcript hash *invokes this assumption*. The z3 gate proves the chip computes 6-round BLAKE3 diff --git a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md index 993b0bda3..1263916b1 100644 --- a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md +++ b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md @@ -97,6 +97,9 @@ sign-off in session). Recorded in the spec (`spec/blake3.typ`, A6R section). **External review (2026-08-06, relayed by the project owner):** the round count was reviewed with external symmetric-cryptography experts — removing one round (7→6) judged comfortable, removing two (7→5) explicitly not. -6 rounds is therefore the endorsed floor; sub-6-round variants are -permanently out of scope. The 7-round instantiation remains available as -the zero-assumption / interop fallback at ~10-12% more per merge. +6 rounds is therefore the endorsed floor. Sub-6 variants are not formally +dead, but they cannot be adopted on the project's own authority — that +would need the experts to sit with the reduced margin specifically +(dedicated cryptanalytic review, not an engineering call). The 7-round +instantiation remains available as the zero-assumption / interop fallback +at ~10-12% more per merge. From 89aeeb8c2b0389e9d21a861c9e3a10a7b1b5704e Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 6 Aug 2026 15:13:40 -0300 Subject: [PATCH 14/14] docs(blake3): record the --full z3 gate as attempted-inconclusive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran z3_blake_verify.py --full on 2026-08-06. It took ~145 min and exited 1 with OVERALL: FAIL, but the failure is a resource limit, not a finding: all four monolithic queries (round, rounds=2, rounds=6, rounds=7) returned `unknown`, not `sat`. `unknown` is z3's timeout return — the checks set a solver timeout and return check() directly (z3_blake_verify.py:320-321, :340-341) while the verdict tests == unsat (line 553), so a timeout scores False and pulls OVERALL down. The four budgets sum to 140 min against ~145 min wall, so every check burned its whole allowance. Nothing was disproven and nothing was proven monolithically. The fast board is unchanged and green: G-function UNSAT, init+feed-forward UNSAT, 5/5 negative controls SAT, 4/4 positive controls SAT including the concrete full 6- and 7-round pipelines against the oracle vectors. Records this in IMPLEMENTATION.md "Gates run" and TRANSCRIPTION-AUDIT.md §6 (which said PENDING) as ATTEMPTED-INCONCLUSIVE — explicitly NOT a pass. DESIGN.md §7 item 11's "run --full's monolithic UNSAT before shipping Rust" precondition is attempted but not satisfied. Also corrects TRANSCRIPTION-AUDIT.md §5 Finding 2, which cited those monolithic UNSATs as backing the per-instance G-coverage argument. They do not back it, so that argument now rests on the concrete positive controls and the per-instance index mutant alone. Remediation either way: rerun with a much larger timeout budget on a server (the run is single-threaded and CPU-bound), and/or restructure the monolithic query as round-by-round induction rather than one flat bit-vector problem. --- thoughts/blake3/TRANSCRIPTION-AUDIT.md | 39 +++++++++++++++++-- thoughts/blake3/blake3-chip/IMPLEMENTATION.md | 33 ++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/thoughts/blake3/TRANSCRIPTION-AUDIT.md b/thoughts/blake3/TRANSCRIPTION-AUDIT.md index 8e4e00cb0..e96cc8b67 100644 --- a/thoughts/blake3/TRANSCRIPTION-AUDIT.md +++ b/thoughts/blake3/TRANSCRIPTION-AUDIT.md @@ -124,7 +124,10 @@ UNSAT (pinned), dropped → SAT (forgeable mod p). Same class, now demonstrated Correctly documented as gate-invisible (no bus layer). - **Finding 2 ("covers every G" is a model argument) — FIX REAL.** §7.11 records it; the positive controls do run the full 48-instance pipeline concretely, and this audit's - per-instance index mutant + the `--full` monolithic UNSATs (§6) back it. + per-instance index mutant backs it. (Superseded in part: this bullet originally also + cited the `--full` monolithic UNSATs as backing. They were run on 2026-08-06 and came + back `unknown` on all four queries — see §6 — so they support nothing either way. The + argument rests on the concrete positive controls and the index mutant.) - **Finding 3 (carry encoding ambiguity `(1,0)`/`(0,1)`) — correctly classified harmless.** The sum identity constrains only `c1+c2`; `s` is pinned regardless. - **Harness defect #1 (banner overstatement) — FIX REAL.** The banner now reads from @@ -139,8 +142,38 @@ UNSAT (pinned), dropped → SAT (forgeable mod p). Same class, now demonstrated - default (`z3_blake_verify.py`), z3 5.0.0: **OVERALL: PASS** (board identical to §9 of DESIGN.md). -- `--full` (monolithic symbolic round / rounds=2 / 6-round / 7-round UNSATs): - **PENDING — fill in when the background run completes.** +- `--full` (monolithic symbolic round / rounds=2 / 6-round / 7-round UNSATs), run + 2026-08-06: **ATTEMPTED-INCONCLUSIVE — no pass, and no counterexample.** The run took + ~145 min and exited 1 (`OVERALL: FAIL`), but all four monolithic queries returned + `unknown`, not `sat`: + + ``` + round (clean) -> unknown (want unsat) + compress rounds=2 -> unknown (want unsat) + compress rounds=6 -> unknown (want unsat) + compress rounds=7 -> unknown (want unsat) + ``` + + `unknown` is z3's resource-limit return (`s.set("timeout", timeout_ms)` then + `s.check()`, `z3_blake_verify.py:320-321`/`:340-341`); the verdict tests `== unsat` + (line 553), so a timeout is scored `False` and pulls OVERALL to FAIL. The four + budgets sum to 140 min against ~145 min wall, i.e. every check burned its full + allowance. **Nothing was disproven; nothing was proven monolithically.** The fast + board is unchanged and green: + + ``` + G-function UNSAT (covers all G) : True + init+feed-forward UNSAT (rounds=0): True + negative controls all SAT : True + positive controls all SAT : True (full 6-/7-round pipeline, concrete) + ``` + + Consequence for §5's Finding 2 above: the "`--full` monolithic UNSATs (§6)" cited + there as backing the per-instance coverage argument did **not** land, so that + argument currently rests on the concrete positive controls and the per-instance + index mutant alone. Remediation: rerun with a much larger timeout budget on a + server (single-threaded, CPU-bound), and/or restructure the monolithic query as + round-by-round induction. ## §6b — Reconciliation with the second, independent audit (`audit_gate_transcription.py`) diff --git a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md index 1263916b1..fda2cd634 100644 --- a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md +++ b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md @@ -60,6 +60,39 @@ vectors + the e2e bus-balance gate check. - the z3 gate (`z3_blake_verify.py`) proves the *design*; the transcription design → Rust is covered by the vectors + e2e, per the gate's own documentation of what it cannot see (§7 items 4, 5, 11). +- **`--full` monolithic UNSATs, run 2026-08-06: ATTEMPTED-INCONCLUSIVE, not + satisfied.** `z3_blake_verify.py --full` ran ~145 min and exited 1 + (`OVERALL: FAIL`). All four monolithic queries hit z3's resource limit: + + ``` + round (clean) -> unknown (want unsat) + compress rounds=2 -> unknown (want unsat) + compress rounds=6 -> unknown (want unsat) + compress rounds=7 -> unknown (want unsat) + ``` + + `unknown` is the timeout return — the checks `s.set("timeout", timeout_ms)` + then `return s.check()` (`z3_blake_verify.py:320-321`, `:340-341`), and the + verdict tests `== unsat` (line 553), so a timeout scores `False` and drags + OVERALL to FAIL. Timing corroborates a clean sweep of timeouts: the budgets + are 30+30+40+40 = 140 min against ~145 min wall. **No counterexample was + found — nothing was disproven — but no monolithic UNSAT was obtained + either.** The fast board is unchanged and fully green: + + ``` + G-function UNSAT (covers all G) : True + init+feed-forward UNSAT (rounds=0): True + negative controls all SAT : True + positive controls all SAT : True (full 6-/7-round pipeline, concrete) + ``` + + DESIGN.md §7 item 11's "run `--full`'s monolithic UNSAT before shipping + Rust" precondition is therefore **attempted but not satisfied**; the + coverage of the 48 unrolled G instances still rests on the concrete + positive controls plus the per-instance index mutant, which do pass. + Remediation: rerun with a much larger timeout budget on a server (the run + is single-threaded and CPU-bound), and/or restructure the monolithic query + as round-by-round induction instead of one flat bit-vector problem. ## Known costs and open items