diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 9d29665e..7e2a8d2b 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,5 +1,89 @@ # ndarray — Epiphanies (append-only) +## 2026-07-29 — Which PACKAGE pulls a dep decides whether it can consume you +**Status:** FINDING +**Scope:** @simd-savant @truth-architect domain:build-graph +**Cross-ref:** PR #267, `.claude/knowledge/the-simd-ladder.md` + +The ladder's goal is that every dependency carrying its own SIMD instead +consumes `ndarray::simd`. Whether a given dependency *can* is not a property +of the dependency, and not of "the workspace" — it is decided by **which +package in the workspace pulls it**: + +| dependency | entry point | can consume `ndarray::simd`? | +|---|---|---| +| `chacha20` | `crates/encryption` → … → `chacha20` | **yes** | +| `curve25519-dalek` | `crates/encryption` → `ed25519-dalek` → … | **yes** | +| `blake3` | **root `ndarray`** (`std` feature) | **no — cycle** | + +Measured — and measured the right way round. The evidence is the **positive** +reverse tree (which terminates at `encryption`, with root `ndarray` absent), +plus a control proving the method can produce a hit: + +```console +$ cargo tree -p encryption -i curve25519-dalek # positive: full path shown +curve25519-dalek v4.1.3 +└── ed25519-dalek v2.2.0 + └── encryption v0.1.0 (crates/encryption) + +$ cargo tree -p ndarray -i blake3 # control: a real edge DOES hit +blake3 v1.8.4 +└── ndarray v0.17.2 (/workspace/ndarray) +``` + +**Not** an error message. A first draft rested on +`cargo tree -p ndarray -i curve25519-dalek` failing with `package ID +specification ... did not match any packages`; a typo produces that message +byte-for-byte, so it cannot distinguish "no such edge" from "no such +package". Corrected by CodeRabbit on #268 — and it is the same defect this +repo keeps hitting one level down: a check that cannot fail for the reason +you think it is failing. + +Only blake3 is pulled by the ROOT package, so only blake3 closes a loop when +it depends back on ndarray. The other two ride a shape that already works in +this repo today — `crates/encryption` → `chacha20` → `ndarray(root)` is +exactly it. + +**Why this is worth recording rather than re-derived.** "ndarray depends on +X, so X can't depend on ndarray" is the intuitive rule and it is wrong at +workspace granularity. A sub-crate is a different package; the cycle is +per-package, not per-workspace. Reasoning at workspace granularity says all +three are blocked, which would have parked two rungs that have no blocker at +all. + +**On the diagnostic — be precise, because two different things were +conflated here.** Cargo *does* report the cycle when the patched package +would be selected; `cargo update -p blake3` printed the chain +(`blake3 ... satisfies dependency of ndarray ... satisfies path dependency +ndarray of blake3`). What is NOT a cycle diagnostic is `[[patch.unused]]`. +That only says the patch was not selected, and the usual causes are a version +that does not satisfy the requirement or a stale lockfile. + +An earlier version of this entry read the unused patch as the cycle's +signature and called the failure "silent." Both halves were wrong, and codex +caught it on #268. Treating `[[patch.unused]]` as a cycle report teaches the +next reader to misdiagnose an ordinary stale patch — and undermines the very +check this entry prescribes. + +The check stands, but ONLY in its positive form — and the wording here was +itself an instance of the bug it warns about, corrected on #268: + +Consequence: **before planning any "make X consume our crate" work, run +`cargo tree -p -i `.** + +1. First confirm `` is a real package in the selected graph (a typo, or a + package absent from that graph, produces the byte-identical error). +2. A tree that resolves and shows the root package = the edge must be cut + first. +3. A tree that resolves and does NOT show it = unblocked. +4. **A package-ID error is inconclusive — never "no cycle".** + +An earlier draft of this very entry said "an error there means no cycle and +the work is unblocked", one paragraph after explaining that the error is +ambiguous. Read literally it would mark blocked work as unblocked on the +strength of a typo. + + ## 2026-07-29 — BLAKE3 needs a method surface, not intrinsics (measured) **Status:** FINDING **Scope:** @simd-savant domain:codec diff --git a/.claude/knowledge/blake3-ab-bench/blake3_ab.rs b/.claude/knowledge/blake3-ab-bench/blake3_ab.rs new file mode 100644 index 00000000..5c01c310 --- /dev/null +++ b/.claude/knowledge/blake3-ab-bench/blake3_ab.rs @@ -0,0 +1,33 @@ +//! A/B: ndarray's in-tree BLAKE3 vs the external `blake3` crate, on the input +//! sizes ndarray actually hashes. Throwaway; deleted after the measurement. +use std::time::Instant; + +fn bench [u8; 32]>(name: &str, iters: u32, mut f: F) -> f64 { + let mut sink = 0u8; + for _ in 0..iters / 10 { sink ^= f()[0]; } // warm + let t = Instant::now(); + for _ in 0..iters { sink ^= f()[0]; } + let ns = t.elapsed().as_nanos() as f64 / iters as f64; + std::hint::black_box(sink); + println!(" {name:<12} {ns:>10.1} ns/op"); + ns +} + +fn main() { + // Sizes chosen to match what this substrate ACTUALLY hashes: + // 16 B a word (crystal_encoder) + // 256 B short text + // 480 B the SoA node's value region (512 - key(16) - edges(16)) + // 512 B THE canonical SoA node -- 4096 bit, the default unit + // 1024 B one BLAKE3 chunk exactly (the hash_many threshold) + // 2 KB VSA_BYTES + // 64 KB bulk + for &n in &[16usize, 256, 480, 512, 1024, 2048, 65536] { + let data: Vec = (0..n).map(|i| (i % 251) as u8).collect(); + let iters = if n >= 65536 { 2_000 } else { 50_000 }; + println!(" input = {n} B"); + let ours = bench("in-tree", iters, || *ndarray::hpc::blake3::hash(&data).as_bytes()); + let theirs = bench("blake3 crate", iters, || *blake3::hash(&data).as_bytes()); + println!(" ratio {:>10.2}x (in-tree / crate)\n", ours / theirs); + } +} diff --git a/.claude/knowledge/blake3-ab-bench/run.sh b/.claude/knowledge/blake3-ab-bench/run.sh new file mode 100755 index 00000000..b6391b9d --- /dev/null +++ b/.claude/knowledge/blake3-ab-bench/run.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# A/B: ndarray's in-tree BLAKE3 vs the external `blake3` crate. +# +# On-demand instrument, same posture as ../simd-codegen-oracle: run it when the +# question is open, record the answer, stop. NOT a CI job. +# +# Requires the external `blake3` crate to still be a dependency -- once it is +# dropped, this bench can no longer build, which is by design: at that point +# there is nothing left to compare against. +set -eu +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../../.." && pwd)" +# The bench needs its source under $REPO/examples/ for cargo to see it, and +# removes it afterwards. Both halves must refuse to touch anything they did not +# create: a fixed destination plus an unconditional `rm` in the EXIT trap would +# clobber a pre-existing examples/blake3_ab.rs and then delete it. +EXAMPLE_DIR="$REPO/examples" +EXAMPLE="$EXAMPLE_DIR/blake3_ab.rs" +created_example_dir=0 +if [ -e "$EXAMPLE" ] || [ -L "$EXAMPLE" ]; then + echo "refusing to overwrite $EXAMPLE" >&2 + exit 1 +fi +if [ ! -d "$EXAMPLE_DIR" ]; then + mkdir "$EXAMPLE_DIR" + created_example_dir=1 +fi +# Only remove the directory if this script created it. +trap 'rm -f "$EXAMPLE"; [ "$created_example_dir" -eq 0 ] || rmdir "$EXAMPLE_DIR" 2>/dev/null || true' EXIT +cp "$HERE/blake3_ab.rs" "$EXAMPLE" +cd "$REPO" +cargo run --release --quiet --example blake3_ab diff --git a/.claude/knowledge/blake3-in-tree-measured.md b/.claude/knowledge/blake3-in-tree-measured.md new file mode 100644 index 00000000..04b46217 --- /dev/null +++ b/.claude/knowledge/blake3-in-tree-measured.md @@ -0,0 +1,175 @@ +# In-tree BLAKE3 — correct, and what it costs + +> **Status: MEASURED, 2026-07-29.** Correctness against the official vectors; +> throughput against the external crate. Both numbers below are reproducible +> with the instruments in this directory. + +## READ BY: +- Anyone about to drop the external `blake3` dependency +- Anyone continuing rung 3 of `the-simd-ladder.md` + +## P0 TRIGGER +About to swap `blake3::` call sites onto `crate::hpc::blake3`? **The swap is +correct but costs 1.3× on typical inputs and ~5× at 64 KB. Read the table.** + +--- + +## Why it exists + +Root `ndarray` depends on `blake3`, so `blake3 → ndarray::simd` is a cargo +cycle — the only rung of the ladder that has one (`the-simd-ladder.md`). +Cutting it means ndarray owning BLAKE3 rather than consuming the crate. + +Scoping finding that made this small: **ndarray's usage is entirely +single-input.** 14 call sites across 8 files use only `hash`, +`Hasher::{new, new_keyed, update, finalize, finalize_xof().fill()}`, +`Hash::as_bytes`, and `Hash` as a signature type. **No `hash_many`.** So the +serial core suffices, and it needs no SIMD at all. + +## Correctness — proven + +`src/hpc/blake3.rs`, 771 lines, transcribed from upstream's own +`reference_impl/reference_impl.rs` (the spec-referenced serial +implementation, BLAKE3 spec §5.1). No `unsafe`, no `core::arch`, no new +dependencies. + +Against the official `test_vectors.json`, vendored to +`src/hpc/blake3_test_vectors.json`: + +- **35/35** cases, unkeyed `hash` **and** keyed `keyed_hash`, +- each checked at **both** 32-byte length and the full extended length via + `finalize_xof().fill()` — 140 assertions, +- input lengths 0 … 102 400. + +Plus: streaming (one `update` vs many 37-byte `update`s over 102 400 bytes), +empty input, and incremental `fill` (7 bytes at a time vs one shot). + +`derive_key` was included too — it is another `Hasher` invocation with +different flags, so it came free. + +## Throughput — the cost, measured + +`sh .claude/knowledge/blake3-ab-bench/run.sh`, release build, two runs: + +| input | in-tree | `blake3` crate | ratio | +|---|---|---|---| +| 16 B (a word) | 134 ns | 100 ns | **1.34–1.39×** | +| 256 B (text) | 437 ns | 350 ns | **1.25–1.29×** | +| 2 KB (`VSA_BYTES`) | 3.4 µs | 2.6 µs | **1.30×** | +| 64 KB (bulk) | 109 µs | 23 µs | **4.7–4.9×** | + +### The `array_chunks` fast path — operator's lead, measured + +Hypothesis (operator, citing the blasgraph JIT-gap precedent): the gap might +be closed by proper use of the existing slice primitives rather than by new +SIMD. The staging path copies every byte **twice** — input → `self.block` → +`block_words` — and for a full block the first copy is pure overhead. + +Implemented as a `crate::simd_ops::array_chunks::` fast path in +`ChunkState::update`, guarded `input.len() > BLOCK_LEN` so a chunk's final +block is never compressed early (it carries `CHUNK_END`). **Measured, three +runs:** + +| input | before | after | change | +|---|---|---|---| +| 16 B | 137 ns | 134 ns | — (never reaches the fast path) | +| 256 B | 445 ns | 437 ns | — | +| **2 KB** | **4322 ns** | **3421 ns** | **−21 %** | +| 64 KB | 114.8 µs | 109.0 µs | −5 % | + +**Verdict: real, and bounded.** The double copy was costing ~21 % at the mid +sizes — not nothing, and free to remove. But it does **not** replace the two +structural gaps: inputs ≤ 1 block never reach the fast path at all, and at +64 KB the copy is noise beside the absent `hash_many`. The ratio at 2 KB +moved 1.34–1.60× → a stable 1.30×; the small-input 1.3× and the bulk 4.8× +both stand. + +So the answer to "does it just need proper `array_chunks` use?" is **partly, +and the part it fixes is now fixed.** Rungs 3b (`hash_many`) and 3c (SIMD +single-compress) remain the load-bearing ones. + +Correctness is gated, not assumed: the official vectors cover every boundary +the fast path turns on — 63/64/65 (the `> BLOCK_LEN` guard itself), +127/128/129, and 1023/1024/1025 (the chunk boundary). + +**The two gaps have different causes, and only one is about `hash_many`.** + +- **The 64 KB gap is `hash_many`.** Above one chunk (1024 B) the crate + switches to its degree-8/16 parallel path with the transpose. We have none. + This is exactly rung 3b, and the `U32x16` shuffle surface merged in #267 is + what it would be built on. +- **The 1.3× small-input gap is NOT.** At 16 B there is a single compression + and no parallelism to be had — the crate is still faster because it + SIMD-accelerates *the single compress itself* (its sse41 backend). Closing + that needs a `U32x4`-shaped compress, which is a rung the ladder plan does + not currently have. Call it 3c. + +So the honest shape is: + +```text +3a in-tree core, correct DONE, costs 1.3x typical / 5x bulk +3b hash_many on U32x16 closes the bulk gap +3c SIMD single-compress (U32x4) closes the small-input gap +``` + +## What this means for the swap + +**The cycle-cut is available now and is correct.** It removes a cargo cycle +and 2,910 lines of second-surface `core::arch`. + +It does **not** remove a C build — `Cargo.toml:213` already sets +`default-features = false, features = ["pure"]`, which removed all C/ASM +compilation back in #264. An earlier revision of this line credited the swap +with that too; overstating the benefit matters here specifically, because +what it is being weighed against is transcribing a cryptographic +implementation. + +**It is not free**, and the previous framing ("removes things, needs no +benchmark to justify") was true about what it *removes* and silent about what +it *costs*. With the numbers in hand that framing is incomplete: this is a +trade, and which side wins depends on how hot ndarray's hashing actually is. + +Where the call sites sit on the curve: `crystal_encoder` hashes a word +(16 B band), `vsa` XOF-expands to 2 KB, `merkle_tree`/`seal`/`spo_bundle` +hash small nodes, `deepnsm`/`compression_curves` small. So ndarray's real +exposure is the **1.3–1.6× band**, not the 5× one — but 1.3× on a hot encoder +path is a real cost, not a rounding error. + +**Not swapped here.** The module lands and is tested; the call sites still +use the external crate. Flipping them is a decision with a measured price +tag, and it is the operator's. + +## One deliberate deviation from upstream, and one restored + +- **Deviated:** transcribed from `reference_impl.rs` rather than + `portable.rs` + `lib.rs`. Upstream ships the reference implementation as + the readable, algorithmically-identical serial version, which is what + "take the serial branch everywhere" reduces to. Correctness is proven by + the vectors. + + An earlier revision of this document guessed that "part of the 1.3× is + likely this choice rather than the SIMD gap — `portable.rs` avoids a + per-block staging copy." **That guess is now separated out and was wrong + for the small-input case.** Removing the staging copy (the `array_chunks` + fast path above) bought 21 % at 2 KB and **nothing at ≤ 1 block**, because + inputs that small never reach the fast path. So the small-input 1.3× is not + the staging copy — it is the crate's SIMD single-compress, as originally + suspected. +- **Restored:** `Hash::eq` is **constant-time**. Upstream uses the + `constant_time_eq` crate; the transcription initially used a plain `==`, + which leaks match-prefix length through timing when a BLAKE3 output is used + as a MAC. Rewritten as an XOR-fold with a `black_box` on the accumulator, + no dependency added. No call site in this crate compares two `Hash` values + today — `seal.rs` compares the truncated `MerkleRoot` — so this is a guard + for future consumers, not a live-leak fix. + +## Not claimed + +- Not that the in-tree version should replace the crate. That is the open + decision this document exists to inform. +- Not that the remaining 1.3× has been fully attributed. The staging-copy + term IS now separated (measured at 21 % of the 2 KB cost, 0 % at ≤ 1 + block); what is left at small inputs is *presumed* to be the crate's SIMD + single-compress, and that has not been isolated by building one. +- Not that the bench is rigorous. It is a warm-loop wall-clock A/B, adequate + for a 1.3× vs 5× distinction and not for anything finer. diff --git a/.claude/knowledge/chacha20-vendoring-blast-radius.md b/.claude/knowledge/chacha20-vendoring-blast-radius.md index ed66ad76..25323644 100644 --- a/.claude/knowledge/chacha20-vendoring-blast-radius.md +++ b/.claude/knowledge/chacha20-vendoring-blast-radius.md @@ -157,9 +157,18 @@ SIGILL on a runner without AVX-512 silicon. `cargo check --target=x86_64-unknown-linux-gnu -p ndarray --features approx,serde,rayon` (the second adding `hpc-extras`) — package-scoped to `ndarray`, so `crates/encryption` and therefore `vendor/chacha20` are outside -it. **No CI job compiles the chacha20 AVX-512 backend**, and none of the -coverage claimed here extends to it. Closing that gap would take an explicit -step such as: +it. **No CI job compiles the chacha20 AVX-512 backend.** + +**Correction (codex, #268): the wasm arm IS covered, and an earlier version of +this document said otherwise.** `ci.yaml:141-142`, inside the `wasm_simd` job, +runs `RUSTFLAGS="-C target-feature=+simd128" cargo build --manifest-path +vendor/chacha20/Cargo.toml --target wasm32-unknown-unknown --lib` — which +selects `backends::ndarray_simd` through the cfg's wasm arm, and whose own +comment calls it "the wasm matryoshka" guard. So the accurate statement is +**arm-specific, not backend-wide**: wasm32+simd128 is compiled and guarded in +CI; the x86_64 avx512f arm is compiled by nothing. + +Closing the AVX-512 gap would take an explicit step such as: ```console $ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Ctarget-cpu=x86-64-v4" \ diff --git a/.claude/knowledge/crypto-lane-status.md b/.claude/knowledge/crypto-lane-status.md index b789e737..5d95866d 100644 --- a/.claude/knowledge/crypto-lane-status.md +++ b/.claude/knowledge/crypto-lane-status.md @@ -86,10 +86,51 @@ Two things make this a genuine finding rather than a shrug: and rotate-by-7. It has the tools and declines to apply them at 64-bit width. +### Confirmed a second and third way: it is the OPERATION, not the idiom + +The measurement above used one source form — `u64::rotate_right(n)`. That +leaves an obvious objection: maybe LLVM declines the *rotate idiom* at 64-bit +width, and an explicit shift-or would vectorize, exactly as it does for u32's +`rotate_left(12)`/`(7)`. AVX2 has `vpsllq`/`vpsrlq`, so the ingredients exist. + +Tested. It does not. + +| probe | spelling | amount | packed | +|---|---|---|---| +| `rot_u64x8` | `u64::rotate_right(n)` | runtime | **0** | +| `shiftor_rot_u64x8` | `(x >> n) \| (x << (64-n))` | runtime | **0** | +| `shiftor_rot_const_u64x8` | explicit shift-or | **const** 32/24/16/63 | **0** | + +The third row is the sharp one. At u32 width a *constant* amount vectorizes +two different ways depending on granularity — byte-granular folds to +`vpshufb`, bit-granular to the shift-or triple — and both are packed. At u64 +width neither happens, for either kind of constant. + +**And a fourth confirmation arrived unasked.** A `blake2b_g_shiftor_u64x8` +probe was written as the direct counterpart to `blake2b_g_u64x8`. It never +appeared in the emitted assembly: LLVM **folded the two functions**, leaving +zero mentions of the shiftor symbol and two call sites pointing at the +survivor. So the two spellings are not merely both-scalar — they are +*byte-identical*. The probe was removed rather than kept as a permanently +failing row, since a probe the compiler cannot distinguish from its own +control measures nothing. + **So the u64 ARX lane is the crate's first intrinsic override that meets the -entry criterion** (a probe proving the generic form fails). AVX-512: -`_mm512_rorv_epi64` / `VPROLVQ`, one instruction. AVX2 / NEON / wasm: write -the `vpsllq`/`vpsrlq`-shaped shift-or explicitly, since LLVM will not. +entry criterion** (a probe proving the generic form fails). + +What SHIPPED, per backend (#268), which is not uniform: + +- **AVX-512:** `_mm512_rolv_epi64` / `_mm512_rorv_epi64` — `VPROLVQ`/`VPRORVQ`, + one instruction. This is the earned override. +- **AVX2 / scalar / nightly:** per-lane loops. NEON and wasm re-export the + scalar `U64x8` and are covered by that arm. + +An earlier draft of this paragraph instructed AVX2/NEON/wasm to "write the +`vpsllq`/`vpsrlq`-shaped shift-or explicitly, since LLVM will not." That +prescription contradicted the fourth confirmation directly above it — LLVM +folded the explicit shift-or into the rotate form, byte-identically, so +writing it out is not known to change anything. **It is an unmeasured future +experiment, not the prescribed implementation**, and it is not what shipped. Contrast with the u32 lane, where hand-writing intrinsics *lost* to the optimizer. Same crate, same week, opposite answers — which is the argument diff --git a/.claude/knowledge/simd-codegen-oracle/baseline-x86_64-v3.toml b/.claude/knowledge/simd-codegen-oracle/baseline-x86_64-v3.toml index 975d3b59..bc5929b0 100644 --- a/.claude/knowledge/simd-codegen-oracle/baseline-x86_64-v3.toml +++ b/.claude/knowledge/simd-codegen-oracle/baseline-x86_64-v3.toml @@ -156,3 +156,37 @@ note = "MEASURED FULLY VECTORIZED, and it closes the Group-D question. Observed [probe.transpose_16x16_composed] expect = "unknown" note = "MEASURED VECTORIZED, with a genuine shuffle network. Observed 79 packed / 0 scalar-lane-arith / 38 loop-control / 41 memory. The 79 packed split into 60 vector moves and 19 real shuffle/permute instructions: vinserti128 x7, vpshufd x3, vpblendd x3, vpunpcklqdq x2, vpermq x2, vunpcklpd x1, vpermpd x1 -- covering all four granularities the network needs, including the 64-bit unpack and the 256-bit cross-lane permute that transpose_stage_u32x16 never exercised. Correctness is CHECKED, not assumed: the driver compares the result against a naive nested-loop transpose and aborts on mismatch, and run.sh now executes the probe binary (--emit asm links nothing, so the assertion would otherwise never run). The check was control-tested by deleting stage::<8>, which makes it fire (exit 101). Honest caveats: unlike the single-stage probe this is NOT clean straight-line code -- LLVM kept loop structure (38 loop-control) and there is real spill traffic (41 memory), which is expected, since sixteen 512-bit vectors of live state are 32 ymm registers against 16 architectural ones. A hand-written intrinsic backend faces the same register pressure. So this measurement establishes that the shuffle network is SYNTHESIZED rather than degraded to scalar copies; it does NOT establish throughput parity with rust_avx2.rs, which needs a benchmark that has not been run." + +# ============================================================================ +# Group E -- UNKNOWN. Is the 4096-bit / 512-byte node a viable DEFAULT lane? +# ============================================================================ + +[probe.arx_node4096] +expect = "unknown" +note = "MEASURED FULLY PACKED, ZERO SPILL. Observed 98 packed / 0 scalar-lane-arith / 0 loop-control / 0 memory. The arithmetic is IDENTICAL to the 8x-lane baseline below: 16 vpaddd + 16 vpxor + 16 vpshufb + 33 vmovdqa in both. The 17-instruction delta (16 vmovaps + 1 vxorps) is the dead zero-init of `[U32x16::splat(0); 8]` in the loop form, not a cost of the wider unit -- a real implementation building the array by value emits neither. Crucially the memory column is ZERO: 128 u32 of live state does NOT spill, because the ARX triple is STREAMING (each of the 8 vectors is independent, so LLVM never needs all 128 lanes live at once). Contrast transpose_16x16_composed, which needs all 16 vectors live simultaneously and pays 41 memory ops. That is the discriminating property for node-wide work: free for elementwise/streaming ops, expensive for whole-node-live ops." + +[probe.arx_lane512_x8] +expect = "unknown" +note = "MEASURED FULLY PACKED. Observed 81 packed / 0 scalar-lane-arith / 0 loop-control / 0 memory: 16 vpaddd + 16 vpxor + 16 vpshufb + 33 vmovdqa. Bit-for-bit identical output to arx_node4096 (asserted in the driver), and identical arithmetic in the emitted code -- so the node-wide unit costs nothing structurally over eight applications of the 512-bit lane." + +# Group C, continued -- the u64 rotate written as an EXPLICIT shift-or rather +# than as `u64::rotate_right`. Group C above tested one SOURCE FORM; this tests +# whether LLVM declines the rotate IDIOM at 64-bit width or declines the +# OPERATION. Decides whether ladder rung 6 needs an intrinsic override. +# +# ANSWER: it declines the OPERATION. Both spellings come back 0 packed, for +# runtime-variable AND compile-time-constant amounts. A third probe +# (blake2b_g_shiftor_u64x8) was written and removed: LLVM FOLDED it into +# blake2b_g_u64x8 -- zero mentions of the shiftor symbol in the emitted asm, +# two call sites on the survivor -- proving the two spellings are byte-identical +# rather than merely both-scalar. A probe the compiler cannot distinguish from +# its own control measures nothing. + +[probe.shiftor_rot_u64x8] +expect = "unknown" +note = "MEASURED SCALAR -- 0 packed / 8 scalar-lane-arith / 19 memory, i.e. indistinguishable from rot_u64x8. Writing the rotate as an explicit (x >> n) | (x << (64-n)) does NOT persuade LLVM to vectorize it, so the refusal is about the 64-bit OPERATION and not about the rotate idiom. Driver asserts equality with u64::rotate_right, so this is the same function in a different spelling." + +[probe.shiftor_rot_const_u64x8] +expect = "unknown" +note = "MEASURED SCALAR -- 0 packed / 8 scalar-lane-arith / 8 memory, with BLAKE2b's COMPILE-TIME-CONSTANT 32/24/16/63. This is the sharper half: at u32 width a constant byte-granular amount folds to vpshufb and a constant bit-granular one folds to shift-or, both packed. At u64 width neither happens, for either kind of constant. A third independent confirmation that the u64 lane is the crate's one earned intrinsic override." + diff --git a/.claude/knowledge/simd-codegen-oracle/probes.rs b/.claude/knowledge/simd-codegen-oracle/probes.rs index f26807b5..2f17dcdf 100644 --- a/.claude/knowledge/simd-codegen-oracle/probes.rs +++ b/.claude/knowledge/simd-codegen-oracle/probes.rs @@ -493,6 +493,111 @@ pub fn transpose_16x16_composed(m: [U32x16; 16]) -> [U32x16; 16] { m } +/// The u64 rotate written as an EXPLICIT shift-or, not as `u64::rotate_right`. +/// +/// Group C measured that `u64::rotate_right(n)` lowers to a scalar `rorq` per +/// lane — 0 packed. But that tested one SOURCE FORM. LLVM already vectorizes +/// u32's `rotate_left(12)`/`(7)` as `vpslld`+`vpsrld`+`vpor`, and AVX2 has the +/// 64-bit equivalents `vpsllq`/`vpsrlq`. So the open question is whether LLVM +/// declines the *rotate idiom* at 64-bit width, or declines the *operation*. +/// +/// If the explicit form vectorizes, rung 6 of the ladder needs no intrinsic +/// override at all — just a differently-spelled body, exactly as rung 2 needed +/// only index loops. If it does not, rung 6 is the crate's first genuinely +/// earned `unsafe` intrinsic. +/// +/// `n` is constrained to `1..=63` by the caller: `x >> 64` is UB on `u64`, so +/// the zero case must be excluded rather than masked, matching how +/// `simd_nightly`'s `U32x16::rotate_left` guards `n % 32 == 0`. +#[inline(always)] +fn shiftor_rotr_u64x8(v: U64x8, n: u32) -> U64x8 { + let a = v.to_array(); + let mut out = [0u64; 8]; + for i in 0..8 { + out[i] = (a[i] >> n) | (a[i] << (64 - n)); + } + U64x8::from_array(out) +} + +/// Explicit shift-or u64 rotate, runtime-variable amount, 8 lanes. +/// +/// Normalizes `n` to `0..64` and short-circuits zero, so this public entry +/// point agrees with `u64::rotate_right` at **every** `u32` input rather than +/// only on `1..=63`. Without it, `n >= 64` reaches `a >> n` — a panic in +/// debug, a wrapping shift in release, and a rotate in neither. +/// +/// The normalization lives HERE and deliberately NOT in `shiftor_rotr_u64x8`: +/// that inner body is the thing being measured, and adding a branch plus a +/// modulo inside it would change the very codegen the oracle exists to +/// observe. Guard at the boundary; measure the bare form. +#[inline(never)] +pub fn shiftor_rot_u64x8(v: U64x8, n: u32) -> U64x8 { + let n = n % 64; + if n == 0 { + return v; + } + shiftor_rotr_u64x8(v, n) +} + +/// Explicit shift-or u64 rotate with COMPILE-TIME-CONSTANT amounts — BLAKE2b's +/// 32/24/16/63. Separated from the runtime-variable probe above because the +/// u32 lane vectorizes constants and variables differently (`vpshufb` for +/// byte-granular constants, shift-or otherwise), so the two cases must be +/// distinguished rather than averaged. +#[inline(never)] +pub fn shiftor_rot_const_u64x8(v: U64x8) -> U64x8 { + let a = shiftor_rotr_u64x8(v, 32); + let b = shiftor_rotr_u64x8(a, 24); + let c = shiftor_rotr_u64x8(b, 16); + shiftor_rotr_u64x8(c, 63) +} + +// ============================================================================ +// Group E — UNKNOWN. Is 4096 bit / 512 byte a viable DEFAULT lane? +// ============================================================================ +// The substrate's canonical node is 4096 bit = 512 byte +// (`key(16) | edges(16) | value(480)`). `U32x16` is 512 BIT — one eighth of +// it. So the question is whether a node-wide unit, `[U32x16; 8]` = 128 u32 +// lanes, stays fully packed and amortizes better than looping eight times +// over `U32x16`. +// +// It cannot be a register type: 512 byte is 8 zmm / 16 ymm, so a node-wide +// lane is necessarily a TILE over the existing lane, exactly as `U32x16` is +// already a polyfill over 2 ymm on avx2. The question is therefore not "does +// it fit" but "does the wider unit cost anything" — if the 8x form emits the +// same instruction count as 8 separate ops with no extra spill, the node +// width is free and can be the default unit; if it spills, it cannot. +// +// 128 u32 of live state is 8 zmm or 16 ymm against 16 architectural +// registers, so spill is the thing to watch — the same pressure that showed +// up as 41 memory ops in `transpose_16x16_composed`. + +/// One ARX triple over a NODE-WIDE unit: `[U32x16; 8]` = 4096 bit = 512 byte. +#[inline(never)] +pub fn arx_node4096(a: [U32x16; 8], b: [U32x16; 8]) -> [U32x16; 8] { + let mut out = [U32x16::splat(0); 8]; + for i in 0..8 { + out[i] = ((a[i] + b[i]) ^ b[i]).rotate_left(16); + } + out +} + +/// The SAME work as `arx_node4096`, expressed as the existing 512-bit lane +/// applied eight times by the caller — the baseline it must not lose to. +#[inline(never)] +pub fn arx_lane512_x8(a: [U32x16; 8], b: [U32x16; 8]) -> [U32x16; 8] { + [ + ((a[0] + b[0]) ^ b[0]).rotate_left(16), + ((a[1] + b[1]) ^ b[1]).rotate_left(16), + ((a[2] + b[2]) ^ b[2]).rotate_left(16), + ((a[3] + b[3]) ^ b[3]).rotate_left(16), + ((a[4] + b[4]) ^ b[4]).rotate_left(16), + ((a[5] + b[5]) ^ b[5]).rotate_left(16), + ((a[6] + b[6]) ^ b[6]).rotate_left(16), + ((a[7] + b[7]) ^ b[7]).rotate_left(16), + ] +} + // ============================================================================ // Driver — runtime-derived inputs, every result consumed. // ============================================================================ @@ -605,6 +710,31 @@ fn main() { ); acc ^= ga.reduce_sum() ^ gb.reduce_sum() ^ gc.reduce_sum() ^ gd.reduce_sum(); + // ---- Group C, explicit shift-or form ---- + let sv = U64x8::from_array(std::array::from_fn(|_| rng.next())); + let sn = 1 + (rng.next() % 63) as u32; + // The explicit form must agree with `u64::rotate_right` -- same function, + // different spelling. A codegen probe that measured a DIFFERENT function + // would be worthless. + let want: [u64; 8] = { + let a = sv.to_array(); + std::array::from_fn(|i| a[i].rotate_right(sn)) + }; + let got = shiftor_rot_u64x8(black_box(sv), black_box(sn)); + assert_eq!(got.to_array(), want, "shift-or rotate != u64::rotate_right"); + acc ^= got.reduce_sum(); + + acc ^= shiftor_rot_const_u64x8(black_box(sv)).reduce_sum(); + + // NOTE: a `blake2b_g_shiftor_u64x8` probe was written here and REMOVED, + // because LLVM folded it into `blake2b_g_u64x8` -- identical machine code, + // zero mentions of the shiftor symbol in the emitted asm, and two call + // sites pointing at the surviving one. That fold is the result: the + // explicit shift-or spelling and `u64::rotate_right` are not merely both + // scalar, they are BYTE-IDENTICAL. A probe the compiler cannot tell apart + // from its own control measures nothing, so it is gone rather than kept + // as a permanently-failing row. + // ---- Group D ---- let mut rand_u32x16 = || U32x16::from_array(std::array::from_fn(|_| rng.next() as u32)); let (b3a, b3b, b3c, b3d) = blake3_g_u32x16( @@ -650,5 +780,21 @@ fn main() { } acc ^= composed.iter().fold(0u64, |s, v| s ^ v.reduce_sum() as u64); + // ---- Group E ---- + let node_a: [U32x16; 8] = std::array::from_fn(|_| rand_u32x16()); + let node_b: [U32x16; 8] = std::array::from_fn(|_| rand_u32x16()); + let n1 = arx_node4096(black_box(node_a), black_box(node_b)); + let n2 = arx_lane512_x8(black_box(node_a), black_box(node_b)); + // Same work, so the two forms must agree bit-for-bit. + for i in 0..8 { + assert_eq!( + n1[i].to_array(), + n2[i].to_array(), + "arx_node4096 != arx_lane512_x8 at vector {i}" + ); + } + acc ^= n1.iter().fold(0u64, |s, v| s ^ v.reduce_sum() as u64); + acc ^= n2.iter().fold(0u64, |s, v| s ^ v.reduce_sum() as u64); + println!("simd-codegen-oracle: probes executed, combined checksum = {acc:#018x}"); } diff --git a/.claude/knowledge/the-simd-ladder.md b/.claude/knowledge/the-simd-ladder.md new file mode 100644 index 00000000..1c4bbdee --- /dev/null +++ b/.claude/knowledge/the-simd-ladder.md @@ -0,0 +1,289 @@ +# The ladder — one audited SIMD surface, nothing above it carrying its own + +> **Status: PLAN, with measured rungs marked.** Every "proven" row below cites +> the measurement. Every "assumed" row says so. + +## READ BY: +- Anyone continuing the BLAKE3 / chacha20 / dalek port work +- Anyone about to add a dependency that carries `core::arch` +- `simd-savant`, `truth-architect` + +## The invariant + +**All SIMD lives once, audited, inside `ndarray::simd`.** A dependency that +ships its own intrinsics is a second unaudited surface — which is what the +matryoshka pattern exists to prevent, and what `.cargo/config.toml` already +neutralizes by hand for one crate (`curve25519_dalek_backend = "serial"`). + +The ladder is the ordered work of making that true rather than aspirational. + +--- + +## The cycle map — the thing that orders everything else + +Whether a dependency can consume `ndarray::simd` is decided by **which +ndarray package pulls it**, and this was measured, not assumed: + +| dependency | entry point | `X → ndarray::simd` cycle? | +|---|---|---| +| `chacha20` | `crates/encryption` → `chacha20poly1305` → `chacha20` | **no** | +| `curve25519-dalek` | `crates/encryption` → `ed25519-dalek` → `curve25519-dalek` | **no** | +| `blake3` | **root `ndarray`** (`std` feature, 11 files in `src/hpc/`) | **YES** | + +The evidence is the **positive** reverse tree, not an error message. Asking +for the full reverse-dependency tree of `curve25519-dalek` shows every path +to it, and the tree terminates at `encryption` — root `ndarray` is nowhere in +it: + +```console +$ cargo tree -p encryption -i curve25519-dalek +curve25519-dalek v4.1.3 +└── ed25519-dalek v2.2.0 + └── encryption v0.1.0 (crates/encryption) +``` + +**Control — the same command shape does produce a hit when the edge exists**, +so the method can discriminate: + +```console +$ cargo tree -p ndarray -i blake3 +blake3 v1.8.4 +└── ndarray v0.17.2 (/workspace/ndarray) +``` + +An earlier version rested this on `cargo tree -p ndarray -i curve25519-dalek` +returning `error: package ID specification ... did not match any packages`. +That is **weak evidence and was corrected** (CodeRabbit, #268): a mistyped +name produces the byte-identical message — + +```console +$ cargo tree -p ndarray -i curve25519-dalekk +error: package ID specification `curve25519-dalekk` did not match any packages +``` + +— so the error cannot distinguish "no such edge" from "no such package". Use +the positive tree, and keep a known-hit control beside it. + +**Only blake3 has a cycle**, because only blake3 is pulled by the *root* +package. `cargo update -p blake3` reports it by naming the chain — the patched +`blake3` satisfies ndarray's dependency, and ndarray satisfies blake3's path +dependency. + +Do **not** read `[[patch.unused]]` as that diagnostic. It only means the patch +was not selected, and the ordinary causes are a version that does not satisfy +the requirement or a stale lockfile. The two were conflated in an earlier +draft (codex, #268); treating the unused patch as a cycle signature teaches +the next reader to misdiagnose a plain stale patch. + +The other two ride a pattern that **already works in this repo today**: +`crates/encryption` → `chacha20` → `ndarray(root)` is exactly that shape. + +--- + +## Rungs + +### 1. `U32x16` ARX — PROVEN, merged + +Add / BitXor / `rotate_left`. Measured at the AVX2 instruction floor: 8 +`vpaddd` for 64 u32 lanes, no scalar op touching lane data, +`rotate_left(16)` strength-reduced to `vpshufb` +(`td-t22-asm-investigation.md`). + +### 2. `U32x16` shuffle surface — PROVEN, merged (#267) + +`interleave_{lo,hi}_u{32,64}`, `concat_{lo,hi}_halves`. Semantics are x86's +per 256-bit half, parity-checked against the real intrinsics and +control-tested. Present on all six backends. + +**The insight that made it right:** two `__m256i` fit in one `U32x16`, and +every operation in BLAKE3's `hash_many` is either lane-wise or confined +*within* a 128-/256-bit lane, so two 8-lane groups never interact and the +algorithm runs at DEGREE 16 with no cross-talk. An earlier attempt built this +on `U32x8` and was wrong (operator-corrected; see EPIPHANIES). + +### 3. BLAKE3 — IN FLIGHT, and the only rung that must cut a cycle + +**Scoping finding:** ndarray's own blake3 usage is *entirely single-input* — +`hash`, `Hasher::{new, new_keyed, update, finalize, finalize_xof().fill()}`. +**No `hash_many`.** So cutting the cycle needs only the **portable** core +(scalar `[u32; 16]` compress + the chunk/tree/XOF state machine), which uses +no SIMD at all. + +That splits the rung: + +- **3a — cut the cycle.** Transcribe portable BLAKE3 into ndarray, drop the + external crate. Removes the cycle and ~2,910 lines of second-surface + `core::arch`. Verified against the official `test_vectors.json`. *No SIMD + involved — correctness only.* + + **It does NOT remove a C build.** `Cargo.toml:213` already sets + `default-features = false, features = ["pure"]`, which removed all C/ASM + compilation in #264 — 33 `.o` objects and `libblake3_avx512_assembly.a`, + measured at the time. Earlier wording here credited 3a with removing a C + build that a previous PR had already removed; codex caught it on #268. + Overstating the benefit matters especially here, because the thing being + weighed against it is transcribing a cryptographic implementation. +- **3b — throughput, optional.** Port `hash_many` onto the rung-2 shuffle + surface for multi-chunk inputs. **This is where a benchmark matters**, and + it is not on the critical path. + +### 4. chacha20 — the wasm arm is live and CI-guarded; the AVX-512 arm is not + +`vendor/chacha20`'s `ndarray_simd` backend has two arms, and they are in +very different states: + +- **wasm32 + `simd128` — compiled and guarded.** `ci.yaml:141-142` builds + `vendor/chacha20` for `wasm32-unknown-unknown` with + `RUSTFLAGS="-C target-feature=+simd128"`, which selects `ndarray_simd`. + The job comment calls it "the wasm matryoshka" guard, and it sits directly + after the node parity step that proves the same `U32x16` lane bit-exact. +- **x86_64 + `avx512f` — compiled by nothing.** No image and no CI job + (`chacha20-vendoring-blast-radius.md`). + +An earlier version of this section said "no CI job compiles it" without +qualification. That was **false** — caught by codex on #268 — and is a fourth +instance of the scope-quantifier error this repo keeps hitting: the x86 path +was checked and the conclusion generalized to all targets. + +Upstream 0.10.1 now ships its own `avx512.rs` in the same niche, unmeasured +against ours. **The AVX-512 arm is blocked on a benchmark, then on the +fork-vs-vendored ruling. The wasm arm is already working.** + +### 5. curve25519-dalek — the largest untouched surface + +57 `_mm*` intrinsic calls under 52 `unsafe` occurrences across +`backend/vector/avx2/field.rs` and `packed_simd.rs`. Currently neutralized +rather than ported: `.cargo/config.toml` sets +`curve25519_dalek_backend = "serial"`, which cfg's the whole `vector` module +out at `backend/mod.rs:42`. + +That is the correct *reachability* answer and costs nothing today (X25519's +Montgomery ladder never touches the vector backend; the vector path serves +only Edwards multi-scalar work this crate doesn't do). Porting it onto +`ndarray::simd` is a real rung, has **no cycle**, and is gated on nothing +except appetite. + +### 6. u64 ARX — the one *earned* intrinsic override, BUILT + +Measured first: `rotate_left`/`rotate_right` did not exist on `U64x8`/`U64x4` +on any of the six backends, and a scalar u64 rotate loop does **not** +vectorize — 0 packed, one scalar `rorq` per lane, even for byte-granular +amounts, even with compile-time-constant counts, on a target that has +`vpsllq`/`vpsrlq` and uses exactly that shift-or for u32 +(`crypto-lane-status.md`). LLVM folded two byte-identical probes together, +which says it declines the 64-bit *operation*, not the rotate *idiom*. + +This is the only place in the crate where a hand-written intrinsic meets +`simd-one-spec-design.md`'s entry criterion. **Shipped**, and what each +backend got is not uniform: + +| backend | implementation | +|---|---| +| avx512 | native `_mm512_rolv_epi64` / `_mm512_rorv_epi64` — **the override** | +| avx2, scalar, nightly | per-lane loops — correct, measured not to vectorize | +| neon, wasm | re-export the scalar `U64x8`, covered by that arm | + +So the earned intrinsic exists on exactly one backend; the others carry a +known cost rather than an oversight. The explicit `vpsllq`/`vpsrlq` shift-or +for avx2 is **not** written — that is the obvious follow-up and is unmeasured. + +`n` is taken mod 64 with an `n == 0` short-circuit, since `x >> 64` is UB on +`u64` and `VPROLVQ` takes its count mod 64; the two agree at every input +rather than only in the interior. + +**Nothing consumes it yet.** BLAKE2b needs it, argon2 needs BLAKE2b, and +neither is built. + +--- + +## Is 4096 bit / 512 byte viable as the DEFAULT unit? — MEASURED, yes + +The substrate's canonical node is 4096 bit = 512 byte +(`key(16) | edges(16) | value(480)`). `U32x16` is 512 **bit** — one eighth of +it. So: does a node-wide unit, `[U32x16; 8]` = 128 u32 lanes, stay packed, or +does it drown in spill? + +It cannot be a register type — 512 byte is 8 zmm / 16 ymm — so a node-wide +lane is necessarily a **tile** over the existing lane, exactly as `U32x16` is +already a polyfill over 2 ymm on avx2. The real question is whether the wider +unit costs anything. + +Measured (`arx_node4096` vs `arx_lane512_x8`, same work, asserted bit-for-bit +identical in the driver): + +| form | packed | scalar | **memory** | arithmetic emitted | +|---|---|---|---|---| +| `[U32x16; 8]` node-wide | 98 | 0 | **0** | 16 `vpaddd`, 16 `vpxor`, 16 `vpshufb`, 33 `vmovdqa` | +| `U32x16` × 8 | 81 | 0 | **0** | *identical* | + +**The arithmetic is the same instruction-for-instruction, and neither spills.** +The 17-instruction delta is `16 vmovaps + 1 vxorps` — the dead zero-init of +`[U32x16::splat(0); 8]` in the loop form, which an implementation building the +array by value does not emit. + +**So the node width is free — for the right shape of work.** The +discriminating property is *liveness*, not width: + +- **Streaming / elementwise** (ARX, bitwise, add-mul): each of the 8 vectors + is independent, LLVM processes them one at a time, and 128 lanes of state + never need to be live simultaneously. **Zero spill.** +- **Whole-node-live** (a transpose): all vectors must be live at once. + `transpose_16x16_composed` pays 41 memory ops for exactly this reason. + +That is the rule to design against: a node-wide default unit is viable, and +the ops that must stay lane-width are the ones that need the whole node +resident. + +## Order + +**There is almost no ordering.** An earlier version drew arrows from 3a to +everything, which contradicted this section's own next sentence and would +have serialized independent work; codex caught it on #268. + +The only real dependency is 3a → 3b, because `hash_many` needs an in-tree +BLAKE3 to live in. Everything else is genuinely parallel: chacha20's backend +already depends on `ndarray`, dalek has no cycle (established above), and the +u64 lane is implemented *inside* `ndarray`. None of the three is unblocked by +anything 3a does. + +```text +3a (cut the cycle) ──> 3b (hash_many throughput) [needs a bench] + +4 (chacha20 AVX-512 arm) [independent — needs a bench + a ruling] +5 (dalek) [independent — no blocker but appetite] +6 (u64 ARX lane) DONE — the rotate exists; BLAKE2b/argon2 do not +``` + +## What is NOT claimed here + +- **Not that any of this is faster.** Every rung is measured at + *instruction class*, never at time. "Emits packed shuffles" is not "beats + the intrinsic backend," and no benchmark of any rung against its upstream + equivalent has been run. +- **Not that rung 5 is a defect today.** `serial` is a correct, deliberate, + documented choice, and the vector backend serves no operation this crate + performs. +- **Not that the ladder must be completed.** Rungs 4–5 are optional; only 3a + removes something (a cycle and an unaudited surface — **not** a C build, + which `features = ["pure"]` already removed in #264; see rung 3a). +- **Not that rung 6 pays off yet.** The rotate is shipped and tested, but no + BLAKE2b and no argon2 consume it, and it has been measured at instruction + class only — never timed against a hand-written BLAKE2b. + +## Open, needing a ruling rather than work + +Carried from `chacha20-vendoring-blast-radius.md` so they are visible in one +place: + +1. Fork vs vendored copy for `vendor/chacha20` — `AdaWorldAPI/stream-ciphers` + is a bare upstream mirror at 0.10.1, vendored is 0.9.1 + one backend, and + moving crosses a `cipher` major. +2. Should the Docker images build `encryption` at all? Neither does today, so + the AEAD path ships in no image. +3. Should CI cover the chacha20 v4 arm? The exact command is recorded; adding + the job is a product change. +4. `Cargo.toml:481`'s comment, whose main clause holds only for builds that + select `encryption`. +5. `blake3` and `curve25519-dalek` both resolve from crates.io while + AdaWorldAPI forks exist — two P0 violations. Rung 3a dissolves the blake3 + half by removing the dependency entirely. diff --git a/src/hpc/blake3.rs b/src/hpc/blake3.rs new file mode 100644 index 00000000..34641b29 --- /dev/null +++ b/src/hpc/blake3.rs @@ -0,0 +1,864 @@ +//! Pure-Rust, dependency-free BLAKE3 transcode. +//! +//! This module is a portable-only (no SIMD, no `unsafe`) transcription of +//! BLAKE3, based on the algorithm as published in the reference +//! implementation shipped by the upstream `BLAKE3-team/BLAKE3` project +//! (`reference_impl/reference_impl.rs`, referenced in Section 5.1 of the +//! BLAKE3 spec). That reference implementation is the serial, non-SIMD +//! tree-building algorithm — exactly the "single degree" code path that +//! `portable.rs` / `lib.rs`'s SIMD-parallel tree-walk (`compress_subtree_wide`, +//! `hash_many`, etc.) reduce to when `simd_degree() == 1`. It produces +//! byte-identical output to every other conformant BLAKE3 implementation; +//! only the parallel batching is left out, which is irrelevant here since we +//! have no SIMD/threading in scope (see module docs at the bottom of this +//! file for what was intentionally not transcribed). +//! +//! No `unsafe`, no `core::arch`, no external crates. `no_std`-friendly aside +//! from the tests, which use `std` (they run only under `cfg(test)`). + +use core::cmp::min; +use core::fmt; + +// --------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------- + +const OUT_LEN: usize = 32; +const KEY_LEN: usize = 32; +const BLOCK_LEN: usize = 64; +const CHUNK_LEN: usize = 1024; + +const CHUNK_START: u32 = 1 << 0; +const CHUNK_END: u32 = 1 << 1; +const PARENT: u32 = 1 << 2; +const ROOT: u32 = 1 << 3; +const KEYED_HASH: u32 = 1 << 4; +const DERIVE_KEY_CONTEXT: u32 = 1 << 5; +const DERIVE_KEY_MATERIAL: u32 = 1 << 6; + +const IV: [u32; 8] = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; + +const MSG_PERMUTATION: [usize; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]; + +// --------------------------------------------------------------------- +// Compression function core (transcribed from portable.rs's `g`/`round`, +// specialized to the reference implementation's single-permutation-array +// style rather than portable.rs's per-round MSG_SCHEDULE table — the two +// are algebraically identical, just indexed differently.) +// --------------------------------------------------------------------- + +#[inline(always)] +fn g(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, mx: u32, my: u32) { + state[a] = state[a].wrapping_add(state[b]).wrapping_add(mx); + state[d] = (state[d] ^ state[a]).rotate_right(16); + state[c] = state[c].wrapping_add(state[d]); + state[b] = (state[b] ^ state[c]).rotate_right(12); + state[a] = state[a].wrapping_add(state[b]).wrapping_add(my); + state[d] = (state[d] ^ state[a]).rotate_right(8); + state[c] = state[c].wrapping_add(state[d]); + state[b] = (state[b] ^ state[c]).rotate_right(7); +} + +#[inline(always)] +fn round(state: &mut [u32; 16], m: &[u32; 16]) { + // 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]); +} + +#[inline(always)] +fn permute(m: &mut [u32; 16]) { + let mut permuted = [0u32; 16]; + for i in 0..16 { + permuted[i] = m[MSG_PERMUTATION[i]]; + } + *m = permuted; +} + +/// The core compression function. Returns the full 16-word state (already +/// XOR-finalized): the first 8 words are the chaining value output, and (when +/// `ROOT` is set) all 16 words are used as extendable-output bytes. +fn compress(chaining_value: &[u32; 8], block_words: &[u32; 16], counter: u64, block_len: u32, flags: u32) -> [u32; 16] { + let counter_low = counter as u32; + let counter_high = (counter >> 32) as u32; + #[rustfmt::skip] + let mut 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, counter_high, block_len, flags, + ]; + let mut block = *block_words; + + round(&mut state, &block); // round 1 + permute(&mut block); + round(&mut state, &block); // round 2 + permute(&mut block); + round(&mut state, &block); // round 3 + permute(&mut block); + round(&mut state, &block); // round 4 + permute(&mut block); + round(&mut state, &block); // round 5 + permute(&mut block); + round(&mut state, &block); // round 6 + permute(&mut block); + round(&mut state, &block); // round 7 + + for i in 0..8 { + state[i] ^= state[i + 8]; + state[i + 8] ^= chaining_value[i]; + } + state +} + +#[inline] +fn first_8_words(compression_output: [u32; 16]) -> [u32; 8] { + let mut out = [0u32; 8]; + out.copy_from_slice(&compression_output[0..8]); + out +} + +fn words_from_little_endian_bytes(bytes: &[u8], words: &mut [u32]) { + debug_assert_eq!(bytes.len(), 4 * words.len()); + for (four_bytes, word) in bytes.chunks_exact(4).zip(words.iter_mut()) { + let mut arr = [0u8; 4]; + arr.copy_from_slice(four_bytes); + *word = u32::from_le_bytes(arr); + } +} + +// --------------------------------------------------------------------- +// Output (chunk / parent finalization; also the extendable-output source) +// --------------------------------------------------------------------- + +/// Each chunk or parent node can produce either an 8-word chaining value or, +/// by setting the `ROOT` flag, any number of final output bytes. `Output` +/// captures the state just prior to choosing between those two possibilities. +#[derive(Clone)] +struct Output { + input_chaining_value: [u32; 8], + block_words: [u32; 16], + counter: u64, + block_len: u32, + flags: u32, +} + +impl Output { + fn chaining_value(&self) -> [u32; 8] { + first_8_words(compress(&self.input_chaining_value, &self.block_words, self.counter, self.block_len, self.flags)) + } + + fn root_output_bytes(&self, out_slice: &mut [u8]) { + let mut output_block_counter = 0u64; + for out_block in out_slice.chunks_mut(2 * OUT_LEN) { + let words = compress( + &self.input_chaining_value, + &self.block_words, + output_block_counter, + self.block_len, + self.flags | ROOT, + ); + // The output length might not be a multiple of 4. + for (word, out_word) in words.iter().zip(out_block.chunks_mut(4)) { + let bytes = word.to_le_bytes(); + out_word.copy_from_slice(&bytes[..out_word.len()]); + } + output_block_counter += 1; + } + } +} + +// --------------------------------------------------------------------- +// ChunkState +// --------------------------------------------------------------------- + +#[derive(Clone)] +struct ChunkState { + chaining_value: [u32; 8], + chunk_counter: u64, + block: [u8; BLOCK_LEN], + block_len: u8, + blocks_compressed: u8, + flags: u32, +} + +impl ChunkState { + fn new(key_words: [u32; 8], chunk_counter: u64, flags: u32) -> Self { + Self { + chaining_value: key_words, + chunk_counter, + block: [0u8; BLOCK_LEN], + block_len: 0, + blocks_compressed: 0, + flags, + } + } + + fn len(&self) -> usize { + BLOCK_LEN * self.blocks_compressed as usize + self.block_len as usize + } + + fn start_flag(&self) -> u32 { + if self.blocks_compressed == 0 { + CHUNK_START + } else { + 0 + } + } + + fn update(&mut self, mut input: &[u8]) { + // This method does NOT split across chunks — `Hasher::update` is what + // caps each call, via `want = CHUNK_LEN - self.chunk_state.len()`. + // That cap lives ~250 lines away and the fast path below silently + // depends on it: without it, `full` could run past the chunk boundary + // and compress more than 16 blocks under one chunk counter, producing + // a wrong-but-plausible hash. + // + // The official vectors would NOT catch that, because they only reach + // this method through `Hasher::update`. So state the invariant where + // it is relied upon instead of trusting a distant caller. + debug_assert!( + self.len() + input.len() <= CHUNK_LEN, + "ChunkState::update overran a chunk: {} + {} > {}", + self.len(), + input.len(), + CHUNK_LEN, + ); + + // Fast path — compress whole blocks straight out of `input`. + // + // The staging path below copies every byte twice: once into + // `self.block`, then again into `block_words`. For a full block that + // first copy is pure overhead. `array_chunks::<64>` walks the input as + // `&[u8; 64]` with no copy at all (it is `as_chunks`, a pointer cast), + // so a full block goes input -> words directly. + // + // The guard is `input.len() > BLOCK_LEN`, strictly greater: BLAKE3 + // must not compress a chunk's FINAL block until it knows whether more + // input follows, because that block carries CHUNK_END. Holding back + // the last <= 64 bytes preserves that, and the official vectors are + // the gate — the 0/1/64/65/1024/1025-byte cases all cross this + // boundary. + if self.block_len == 0 && input.len() > BLOCK_LEN { + let full = (input.len() - 1) / BLOCK_LEN; // never the last block + let (head, tail) = input.split_at(full * BLOCK_LEN); + for block in crate::simd_ops::array_chunks::(head) { + let mut block_words = [0u32; 16]; + words_from_little_endian_bytes(block, &mut block_words); + self.chaining_value = first_8_words(compress( + &self.chaining_value, + &block_words, + self.chunk_counter, + BLOCK_LEN as u32, + self.flags | self.start_flag(), + )); + self.blocks_compressed += 1; + } + input = tail; + } + + while !input.is_empty() { + // If the block buffer is full, compress it and clear it. More + // input is coming, so this compression is not CHUNK_END. + if self.block_len as usize == BLOCK_LEN { + let mut block_words = [0u32; 16]; + words_from_little_endian_bytes(&self.block, &mut block_words); + self.chaining_value = first_8_words(compress( + &self.chaining_value, + &block_words, + self.chunk_counter, + BLOCK_LEN as u32, + self.flags | self.start_flag(), + )); + self.blocks_compressed += 1; + self.block = [0u8; BLOCK_LEN]; + self.block_len = 0; + } + + // Copy input bytes into the block buffer. + let want = BLOCK_LEN - self.block_len as usize; + let take = min(want, input.len()); + self.block[self.block_len as usize..][..take].copy_from_slice(&input[..take]); + self.block_len += take as u8; + input = &input[take..]; + } + } + + fn output(&self) -> Output { + let mut block_words = [0u32; 16]; + words_from_little_endian_bytes(&self.block, &mut block_words); + Output { + input_chaining_value: self.chaining_value, + block_words, + counter: self.chunk_counter, + block_len: self.block_len as u32, + flags: self.flags | self.start_flag() | CHUNK_END, + } + } +} + +impl fmt::Debug for ChunkState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ChunkState") + .field("len", &self.len()) + .field("chunk_counter", &self.chunk_counter) + .field("flags", &self.flags) + .finish() + } +} + +fn parent_output(left_child_cv: [u32; 8], right_child_cv: [u32; 8], key_words: [u32; 8], flags: u32) -> Output { + let mut block_words = [0u32; 16]; + block_words[..8].copy_from_slice(&left_child_cv); + block_words[8..].copy_from_slice(&right_child_cv); + Output { + input_chaining_value: key_words, + block_words, + counter: 0, // Always 0 for parent nodes. + block_len: BLOCK_LEN as u32, // Always BLOCK_LEN (64) for parent nodes. + flags: PARENT | flags, + } +} + +fn parent_cv(left_child_cv: [u32; 8], right_child_cv: [u32; 8], key_words: [u32; 8], flags: u32) -> [u32; 8] { + parent_output(left_child_cv, right_child_cv, key_words, flags).chaining_value() +} + +// --------------------------------------------------------------------- +// Public API: Hash +// --------------------------------------------------------------------- + +/// A 32-byte BLAKE3 output. +#[derive(Clone, Copy, Eq)] +pub struct Hash([u8; OUT_LEN]); + +impl Hash { + /// The raw bytes of this `Hash`. + #[inline] + pub const fn as_bytes(&self) -> &[u8; OUT_LEN] { + &self.0 + } +} + +impl From<[u8; OUT_LEN]> for Hash { + #[inline] + fn from(bytes: [u8; OUT_LEN]) -> Self { + Self(bytes) + } +} + +impl From for [u8; OUT_LEN] { + #[inline] + fn from(hash: Hash) -> Self { + hash.0 + } +} + +impl AsRef<[u8]> for Hash { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl PartialEq for Hash { + /// Constant-time comparison. + /// + /// A BLAKE3 output is used as a MAC in keyed mode, so a short-circuiting + /// `==` leaks match-prefix length through timing. Upstream reaches for + /// the `constant_time_eq` crate; this crate takes no new dependencies, so + /// the fold is written here: accumulate the XOR of every byte pair and + /// test once at the end, with `core::hint::black_box` on the accumulator + /// to stop the optimizer reintroducing an early exit. + /// + /// `black_box` is a **best-effort optimizer barrier, not a guarantee** — + /// its documentation is explicit that it provides no formal contract. The + /// data-independent loop above is the real property; the barrier only + /// discourages LLVM from undoing it. A caller needing an audited + /// guarantee should use a dedicated constant-time crate. + /// + /// No call site in this crate currently compares two `Hash` values — + /// `seal.rs` compares the truncated `MerkleRoot` instead — so this is a + /// guard for future consumers rather than a fix for a live leak. + #[inline] + fn eq(&self, other: &Hash) -> bool { + let mut diff = 0u8; + for i in 0..OUT_LEN { + diff |= self.0[i] ^ other.0[i]; + } + // Optimizer barrier (best-effort): discourages LLVM from proving an + // early return equivalent and reintroducing the short circuit. + core::hint::black_box(diff) == 0 + } +} + +impl fmt::Debug for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Hash(")?; + for byte in self.0.iter() { + write!(f, "{:02x}", byte)?; + } + write!(f, ")") + } +} + +// --------------------------------------------------------------------- +// Public API: Hasher +// --------------------------------------------------------------------- + +/// An incremental BLAKE3 hasher. +#[derive(Clone)] +pub struct Hasher { + chunk_state: ChunkState, + key_words: [u32; 8], + cv_stack: [[u32; 8]; 54], // 2^54 * CHUNK_LEN = 2^64 + cv_stack_len: u8, + flags: u32, +} + +impl fmt::Debug for Hasher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Hasher") + .field("chunk_state", &self.chunk_state) + .field("cv_stack_len", &self.cv_stack_len) + .field("flags", &self.flags) + .finish() + } +} + +impl Default for Hasher { + fn default() -> Self { + Self::new() + } +} + +impl Hasher { + fn new_internal(key_words: [u32; 8], flags: u32) -> Self { + Self { + chunk_state: ChunkState::new(key_words, 0, flags), + key_words, + cv_stack: [[0u32; 8]; 54], + cv_stack_len: 0, + flags, + } + } + + /// Construct a new `Hasher` for the regular hash function. + pub fn new() -> Self { + Self::new_internal(IV, 0) + } + + /// Construct a new `Hasher` for the keyed hash function. + pub fn new_keyed(key: &[u8; KEY_LEN]) -> Self { + let mut key_words = [0u32; 8]; + words_from_little_endian_bytes(key, &mut key_words); + Self::new_internal(key_words, KEYED_HASH) + } + + /// Construct a new `Hasher` for the key-derivation function. The context + /// string should be hardcoded, globally unique, and application-specific. + pub fn new_derive_key(context: &str) -> Self { + let mut context_hasher = Self::new_internal(IV, DERIVE_KEY_CONTEXT); + context_hasher.update(context.as_bytes()); + let context_key = context_hasher.finalize(); + let mut context_key_words = [0u32; 8]; + words_from_little_endian_bytes(context_key.as_bytes(), &mut context_key_words); + Self::new_internal(context_key_words, DERIVE_KEY_MATERIAL) + } + + fn push_stack(&mut self, cv: [u32; 8]) { + self.cv_stack[self.cv_stack_len as usize] = cv; + self.cv_stack_len += 1; + } + + fn pop_stack(&mut self) -> [u32; 8] { + self.cv_stack_len -= 1; + self.cv_stack[self.cv_stack_len as usize] + } + + // Section 5.1.2 of the BLAKE3 spec explains this algorithm in detail. + fn add_chunk_chaining_value(&mut self, mut new_cv: [u32; 8], mut total_chunks: u64) { + while total_chunks & 1 == 0 { + new_cv = parent_cv(self.pop_stack(), new_cv, self.key_words, self.flags); + total_chunks >>= 1; + } + self.push_stack(new_cv); + } + + /// Add input to the hash state. May be called any number of times. + pub fn update(&mut self, mut input: &[u8]) -> &mut Self { + while !input.is_empty() { + if self.chunk_state.len() == CHUNK_LEN { + let chunk_cv = self.chunk_state.output().chaining_value(); + let total_chunks = self.chunk_state.chunk_counter + 1; + self.add_chunk_chaining_value(chunk_cv, total_chunks); + self.chunk_state = ChunkState::new(self.key_words, total_chunks, self.flags); + } + + let want = CHUNK_LEN - self.chunk_state.len(); + let take = min(want, input.len()); + self.chunk_state.update(&input[..take]); + input = &input[take..]; + } + self + } + + fn final_output(&self) -> Output { + let mut output = self.chunk_state.output(); + let mut parent_nodes_remaining = self.cv_stack_len as usize; + while parent_nodes_remaining > 0 { + parent_nodes_remaining -= 1; + output = parent_output( + self.cv_stack[parent_nodes_remaining], + output.chaining_value(), + self.key_words, + self.flags, + ); + } + output + } + + /// Finalize the hash and return the default 32-byte output. + pub fn finalize(&self) -> Hash { + let mut out = [0u8; OUT_LEN]; + self.final_output().root_output_bytes(&mut out); + Hash(out) + } + + /// Finalize the hash as an extendable-output stream. + pub fn finalize_xof(&self) -> OutputReader { + OutputReader { + inner: self.final_output(), + position: 0, + } + } +} + +// --------------------------------------------------------------------- +// Public API: OutputReader (extendable output) +// --------------------------------------------------------------------- + +/// A reader for extendable BLAKE3 output, produced by [`Hasher::finalize_xof`]. +#[derive(Clone)] +pub struct OutputReader { + inner: Output, + position: u64, +} + +impl fmt::Debug for OutputReader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutputReader") + .field("position", &self.position) + .finish() + } +} + +impl OutputReader { + /// Fill `buf` with the next `buf.len()` bytes of output. Successive calls + /// continue from where the previous call left off. + pub fn fill(&mut self, mut buf: &mut [u8]) { + const BLOCK: u64 = 2 * OUT_LEN as u64; // 64 + while !buf.is_empty() { + let block_counter = self.position / BLOCK; + let block_offset = (self.position % BLOCK) as usize; + + let words = compress( + &self.inner.input_chaining_value, + &self.inner.block_words, + block_counter, + self.inner.block_len, + self.inner.flags | ROOT, + ); + let mut block_bytes = [0u8; BLOCK as usize]; + for (word, out_word) in words.iter().zip(block_bytes.chunks_mut(4)) { + out_word.copy_from_slice(&word.to_le_bytes()); + } + + let available = BLOCK as usize - block_offset; + let take = min(available, buf.len()); + buf[..take].copy_from_slice(&block_bytes[block_offset..block_offset + take]); + self.position += take as u64; + + let (_, rest) = buf.split_at_mut(take); + buf = rest; + } + } +} + +// --------------------------------------------------------------------- +// Public API: free functions +// --------------------------------------------------------------------- + +/// The default hash function. +pub fn hash(input: &[u8]) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(input); + hasher.finalize() +} + +/// The keyed hash function. +pub fn keyed_hash(key: &[u8; KEY_LEN], input: &[u8]) -> Hash { + let mut hasher = Hasher::new_keyed(key); + hasher.update(input); + hasher.finalize() +} + +/// The key-derivation function. +pub fn derive_key(context: &str, key_material: &[u8]) -> [u8; OUT_LEN] { + let mut hasher = Hasher::new_derive_key(context); + hasher.update(key_material); + *hasher.finalize().as_bytes() +} + +// ======================================================================= +// NOT transcribed (out of scope for this module — documented per task spec): +// +// - `hash_many` / `compress_chunks_parallel` / `compress_parents_parallel` / +// `compress_subtree_wide` / `compress_subtree_to_parent_node`: these exist +// purely to batch multiple chunks/parents through SIMD-width compression +// calls (`platform.simd_degree()`), and to enable Rayon multithreading via +// the `join::Join` trait. With `simd_degree() == 1` and no threading, they +// reduce to exactly the serial one-chunk-then-merge-up-the-stack algorithm +// this module implements directly (`Hasher::update` / `add_chunk_chaining_value`). +// - `update_rayon` and anything behind the `rayon` feature. +// - `zeroize` support (feature-gated in the original; secrets are not a +// concern for ndarray's usage of this module). +// - The `constant_time_eq` CRATE — the dependency, NOT the behaviour. The +// constant-time compare itself IS implemented: see `impl PartialEq for +// Hash`, which XOR-folds all 32 bytes and tests once, so `Hash::eq` keeps +// upstream's timing property without adding the dependency. +// +// An earlier revision of this list claimed the opposite — "`PartialEq` here +// is a normal short-circuiting byte-array compare" — left stale when the +// fold landed. Documenting a timing property backwards is worse than +// omitting it: someone auditing a MAC comparison would have believed this +// file leaks match-prefix length when it does not. +// - Hex encoding/decoding (`to_hex`/`from_hex`) and `Display`/`FromStr`: not +// in the required API list. +// ======================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use std::string::String; + use std::vec::Vec; + + const VECTORS_JSON: &str = include_str!("blake3_test_vectors.json"); + const KEY: &[u8; 32] = b"whats the Elvish word for friend"; + + struct Case { + input_len: usize, + hash_hex: String, + keyed_hash_hex: String, + derive_key_hex: String, + } + + /// The context string the official vectors' `derive_key` outputs were + /// generated with (the `context_string` field of `test_vectors.json`). + /// + /// Deliberately NOT named `DERIVE_KEY_CONTEXT` — that is a `u32` domain + /// flag at module scope, and reusing the name here would shadow it inside + /// this module. + const VECTORS_CONTEXT: &str = "BLAKE3 2019-12-27 16:29:52 test vectors context"; + + /// Hand-rolled extraction of the fields we need from the official BLAKE3 + /// test_vectors.json, without pulling in serde. The file's `cases` array + /// is a flat sequence of objects each with exactly the fields + /// `input_len` (integer), `hash` (hex string), `keyed_hash` (hex string), + /// `derive_key` (hex string, unused here). We scan for each field by name + /// in order, which is sufficient given the known, fixed shape of this + /// file (verified above by inspection). + fn parse_cases(json: &str) -> Vec { + let mut cases = Vec::new(); + let mut rest = json; + loop { + let Some(idx) = rest.find("\"input_len\":") else { + break; + }; + rest = &rest[idx + "\"input_len\":".len()..]; + let len_end = rest + .find(|c: char| c == ',' || c == '\n' || c == '}') + .expect("malformed input_len"); + let input_len: usize = rest[..len_end].trim().parse().expect("bad input_len"); + + let hash_idx = rest.find("\"hash\":").expect("missing hash field") + "\"hash\":".len(); + rest = &rest[hash_idx..]; + let hash_hex = extract_quoted(rest); + rest = &rest[hash_hex.len() + 2..]; + + let kh_idx = rest + .find("\"keyed_hash\":") + .expect("missing keyed_hash field") + + "\"keyed_hash\":".len(); + rest = &rest[kh_idx..]; + let keyed_hash_hex = extract_quoted(rest); + rest = &rest[keyed_hash_hex.len() + 2..]; + + let dk_idx = rest + .find("\"derive_key\":") + .expect("missing derive_key field") + + "\"derive_key\":".len(); + rest = &rest[dk_idx..]; + let derive_key_hex = extract_quoted(rest); + rest = &rest[derive_key_hex.len() + 2..]; + + cases.push(Case { + input_len, + hash_hex, + keyed_hash_hex, + derive_key_hex, + }); + } + cases + } + + /// Given a string starting with optional whitespace then a `"..."` + /// quoted JSON string (no escapes present in this file's hex fields), + /// return the content between the quotes. + fn extract_quoted(s: &str) -> String { + let s = s.trim_start(); + assert!(s.starts_with('"'), "expected quoted string"); + let s = &s[1..]; + let end = s.find('"').expect("unterminated string"); + s[..end].to_string() + } + + fn hex_decode(s: &str) -> Vec { + assert_eq!(s.len() % 2, 0); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() + } + + /// Reference input: byte i is (i % 251). + fn reference_input(len: usize) -> Vec { + (0..len).map(|i| (i % 251) as u8).collect() + } + + #[test] + fn official_test_vectors_all_three_modes() { + let cases = parse_cases(VECTORS_JSON); + assert!(!cases.is_empty(), "no cases parsed"); + let mut checked = 0usize; + for case in &cases { + let input = reference_input(case.input_len); + let expected_hash = hex_decode(&case.hash_hex); + let expected_keyed = hex_decode(&case.keyed_hash_hex); + + // --- unkeyed hash, 32-byte default output --- + let got = hash(&input); + assert_eq!(got.as_bytes()[..], expected_hash[..32], "hash() mismatch at input_len={}", case.input_len); + + // --- unkeyed hash, extended output via finalize_xof --- + let mut hasher = Hasher::new(); + hasher.update(&input); + let mut xof = hasher.finalize_xof(); + let mut extended = std::vec![0u8; expected_hash.len()]; + xof.fill(&mut extended); + assert_eq!( + extended, expected_hash, + "finalize_xof extended output mismatch at input_len={}", + case.input_len + ); + + // --- keyed hash, 32-byte default output --- + let got_keyed = keyed_hash(KEY, &input); + assert_eq!( + got_keyed.as_bytes()[..], + expected_keyed[..32], + "keyed_hash() mismatch at input_len={}", + case.input_len + ); + + // --- keyed hash, extended output via finalize_xof --- + let mut khasher = Hasher::new_keyed(KEY); + khasher.update(&input); + let mut kxof = khasher.finalize_xof(); + let mut kextended = std::vec![0u8; expected_keyed.len()]; + kxof.fill(&mut kextended); + assert_eq!( + kextended, expected_keyed, + "keyed finalize_xof extended output mismatch at input_len={}", + case.input_len + ); + + // --- derive_key, 32-byte default output --- + // + // The third public mode, and the one whose flags are easiest to + // get wrong while the other two still pass: it is a TWO-pass + // construction (DERIVE_KEY_CONTEXT over the context string, + // whose output becomes the key words for a DERIVE_KEY_MATERIAL + // pass over the material). A single-pass transcription, or one + // that swapped the two flags, would be invisible to every + // assertion above. + let expected_dk = hex_decode(&case.derive_key_hex); + let got_dk = derive_key(VECTORS_CONTEXT, &input); + assert_eq!(got_dk[..], expected_dk[..32], "derive_key() mismatch at input_len={}", case.input_len); + + // --- derive_key, extended output via finalize_xof --- + let mut dhasher = Hasher::new_derive_key(VECTORS_CONTEXT); + dhasher.update(&input); + let mut dxof = dhasher.finalize_xof(); + let mut dextended = std::vec![0u8; expected_dk.len()]; + dxof.fill(&mut dextended); + assert_eq!( + dextended, expected_dk, + "derive_key finalize_xof extended output mismatch at input_len={}", + case.input_len + ); + + checked += 1; + } + // Sanity: make sure we actually walked the whole official vector file. + assert_eq!(checked, 35, "expected 35 official test vector cases"); + } + + #[test] + fn streaming_matches_single_shot() { + let input = reference_input(102_400); + let whole = hash(&input); + + let mut hasher = Hasher::new(); + for chunk in input.chunks(37) { + hasher.update(chunk); + } + let streamed = hasher.finalize(); + assert_eq!(whole, streamed); + } + + #[test] + fn empty_input() { + let h = hash(&[]); + // From the official test vectors, input_len == 0 case (the first + // 32 bytes of its "hash" field). + let cases = parse_cases(VECTORS_JSON); + let case0 = cases.iter().find(|c| c.input_len == 0).unwrap(); + let expected = hex_decode(&case0.hash_hex); + assert_eq!(h.as_bytes()[..], expected[..32]); + } + + #[test] + fn fill_in_small_increments_matches_one_shot() { + let input = reference_input(5000); + let mut hasher = Hasher::new(); + hasher.update(&input); + + let mut one_shot = [0u8; 300]; + hasher.finalize_xof().fill(&mut one_shot); + + let mut incremental = [0u8; 300]; + let mut xof = hasher.finalize_xof(); + for chunk in incremental.chunks_mut(7) { + xof.fill(chunk); + } + assert_eq!(one_shot[..], incremental[..]); + } +} diff --git a/src/hpc/blake3_test_vectors.json b/src/hpc/blake3_test_vectors.json new file mode 100644 index 00000000..f6da9179 --- /dev/null +++ b/src/hpc/blake3_test_vectors.json @@ -0,0 +1,217 @@ +{ + "_comment": "Each test is an input length and three outputs, one for each of the hash, keyed_hash, and derive_key modes. The input in each case is filled with a repeating sequence of 251 bytes: 0, 1, 2, ..., 249, 250, 0, 1, ..., and so on. The key used with keyed_hash is the 32-byte ASCII string \"whats the Elvish word for friend\", also given in the `key` field below. The context string used with derive_key is the ASCII string \"BLAKE3 2019-12-27 16:29:52 test vectors context\", also given in the `context_string` field below. Outputs are encoded as hexadecimal. Each case is an extended output, and implementations should also check that the first 32 bytes match their default-length output.", + "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" + } + ] +} diff --git a/src/hpc/mod.rs b/src/hpc/mod.rs index 1c47654f..631c4d42 100644 --- a/src/hpc/mod.rs +++ b/src/hpc/mod.rs @@ -53,6 +53,19 @@ pub mod fingerprint; // rule (carriers in simd_soa, slicing/ops in simd_ops). #[allow(missing_docs)] pub mod plane; + +// In-tree BLAKE3 — see `.claude/knowledge/blake3-in-tree-measured.md`. +// +// A plain `//` comment, not `///`: this is repo incident history, and an outer +// doc comment here would be concatenated into the published rustdoc for the +// module. +// +// The module is fully documented, so it is deliberately NOT under the +// `#[allow(missing_docs)]` that guards `plane`. An earlier revision declared +// it on the line directly after that attribute, which silently re-targeted the +// attribute onto this module and left `plane` unguarded — seven CI jobs failed +// on `missing_docs` errors in a file the change never touched. +pub mod blake3; #[allow(missing_docs)] pub mod seal; #[allow(missing_docs)] diff --git a/src/simd.rs b/src/simd.rs index 04a2735c..554a0ca9 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -743,6 +743,58 @@ mod tests { } } + /// The u64 ARX rotate — `U64x8::rotate_left` / `rotate_right`, the lane + /// BLAKE2b (and therefore argon2) needs. + /// + /// Runs on whichever tier this build compiled, so it gates the native + /// AVX-512 `VPROLVQ`/`VPRORVQ` override and the portable scalar arms + /// against the *same* reference — per-lane `u64::rotate_left`. That + /// matters more here than for the u32 lane, because unlike every other + /// lane-wise op in this crate the two implementations are genuinely + /// different code, not one source form compiled twice. + /// + /// Covers BLAKE2b's own amounts (32/24/16/63) plus the edges, and asserts + /// `rotr(n) == rotl(64 - n)` for each — the identity the two methods' + /// equivalence rests on. + #[test] + fn u64x8_arx_rotate_matches_scalar() { + use super::U64x8; + + let a_arr: [u64; 8] = [ + 0x0123_4567_89AB_CDEF, 0xFFFF_FFFF_FFFF_FFFF, 0x0000_0000_0000_0001, 0x8000_0000_0000_0000, + 0xDEAD_BEEF_CAFE_BABE, 0x0000_0000_FFFF_FFFF, 0xAAAA_AAAA_5555_5555, 0x0F0F_0F0F_F0F0_F0F0, + ]; + let a = U64x8::from_array(a_arr); + + // BLAKE2b uses 32/24/16/63; 0 and 63 are the edges, and 64 must wrap + // to the identity rather than shifting by the full width (UB on u64). + // 65/127/128/191 pin the documented mod-64 contract: an implementation + // that only special-cased the width would pass at 64 and fail here. + // The per-lane oracle is std's own `u64::rotate_left`, which is + // specified to take its count mod 64, so this asserts agreement with + // the language rather than with our own restatement of the rule. + for n in [0u32, 1, 7, 16, 24, 31, 32, 63, 64, 65, 127, 128, 191] { + let l = a.rotate_left(n).to_array(); + let r = a.rotate_right(n).to_array(); + for i in 0..8 { + assert_eq!(l[i], a_arr[i].rotate_left(n), "lane {i} rotate_left({n})"); + assert_eq!(r[i], a_arr[i].rotate_right(n), "lane {i} rotate_right({n})"); + } + // rotr(n) == rotl(64 - n): the identity the pair rests on. + let back = a.rotate_left((64 - (n % 64)) % 64).to_array(); + assert_eq!(r, back, "rotate_right({n}) != rotate_left(64 - {n})"); + } + + // Round-trip: rotating out and back is the identity for every amount. + for n in [1u32, 16, 24, 32, 63] { + assert_eq!( + a.rotate_left(n).rotate_right(n).to_array(), + a_arr, + "rotate_left({n}) then rotate_right({n}) is not the identity" + ); + } + } + /// The BLAKE3 shuffle surface on `U32x16`, checked against the REAL x86 /// intrinsics it reproduces — applied to each 256-bit half. /// diff --git a/src/simd_avx2.rs b/src/simd_avx2.rs index b805eee6..cd3b1e3a 100644 --- a/src/simd_avx2.rs +++ b/src/simd_avx2.rs @@ -1542,6 +1542,54 @@ avx2_int_type!(U16x32, u16, 32, 0u16); avx2_int_type!(U32x16, u32, 16, 0u32); avx2_int_type!(U64x8, u64, 8, 0u64); +/// u64 ARX rotate — the BLAKE2b / argon2 lane. +/// +/// Scalar per-lane loops, and **measured not to vectorize**: the codegen +/// oracle tried three spellings (`u64::rotate_right`, an explicit shift-or +/// with a runtime amount, and the same with BLAKE2b's constants 32/24/16/63) +/// and every one came back 0 packed, one `rorq` per lane. LLVM declines the +/// 64-bit *operation*, not the rotate *idiom* — it folded two of the probes +/// into byte-identical code. +/// +/// So unlike every other lane-wise op in this crate, the scalar spec is NOT +/// the implementation here. The native `VPROLVQ`/`VPRORVQ` override lives on +/// `simd_avx512`'s `U64x8`, which is a real `__m512i`; these arms are the +/// correct-but-unvectorized fallback, and that is a known cost rather than an +/// oversight. See `.claude/knowledge/crypto-lane-status.md`. +impl U64x8 { + /// Lane-wise left-rotate by `n` bits. `n` is taken mod 64. + #[inline(always)] + pub fn rotate_left(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + let a = self.to_array(); + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = a[i].rotate_left(n); + } + Self::from_array(o) + } + + /// Lane-wise right-rotate by `n` bits — BLAKE2b's direction. + /// `rotr(n) == rotl(64 - n)` exactly; kept distinct because BLAKE2b and + /// argon2 are specified in terms of right rotation. + #[inline(always)] + pub fn rotate_right(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + let a = self.to_array(); + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = a[i].rotate_right(n); + } + Self::from_array(o) + } +} + /// BLAKE3 `hash_many` shuffle surface — the transpose network's four unpacks /// and two half-concatenations, at DEGREE 16. /// diff --git a/src/simd_avx512.rs b/src/simd_avx512.rs index c9eba078..8411d4db 100644 --- a/src/simd_avx512.rs +++ b/src/simd_avx512.rs @@ -1636,6 +1636,52 @@ pub struct U64x8(pub __m512i); impl U64x8 { pub const LANES: usize = 8; + /// Lane-wise left-rotate by `n` bits — the u64 ARX rotate. + /// + /// **This is the crate's one earned intrinsic override.** The codegen + /// oracle measured that a scalar u64 rotate loop does NOT vectorize, on + /// three independent spellings: `u64::rotate_right(n)`, an explicit + /// `(x >> n) | (x << (64 - n))` with a runtime amount, and the same with + /// BLAKE2b's compile-time constants 32/24/16/63. All three came back + /// 0 packed / one scalar `rorq` per lane — and LLVM folded two of the + /// probes into byte-identical code, proving it declines the 64-bit + /// *operation* rather than the rotate *idiom*. Contrast the u32 lane, + /// where hand-written intrinsics LOSE to the optimizer + /// (`.claude/knowledge/td-t22-asm-investigation.md`). + /// + /// `n` is taken mod 64; `n == 0` returns `self` unchanged, because + /// `x >> 64` is UB on `u64` and `VPROLVQ`'s own count is taken mod 64. + /// + /// See `.claude/knowledge/crypto-lane-status.md` — BLAKE2b needs this, + /// and argon2 needs BLAKE2b. + #[inline(always)] + pub fn rotate_left(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + // SAFETY: `Self` is a native `__m512i`; this arm is compiled only + // under the avx512f dispatch, the same guarantee every other method + // on this type relies on. + Self(unsafe { _mm512_rolv_epi64(self.0, _mm512_set1_epi64(n as i64)) }) + } + + /// Lane-wise right-rotate by `n` bits — BLAKE2b's rotation direction. + /// + /// `rotr(n) == rotl(64 - n)` exactly, since rotation is modular; this is + /// a distinct method rather than a caller-side subtraction because + /// BLAKE2b/argon2 are specified in terms of right rotation, and + /// `VPRORVQ` is a single instruction in its own right. + #[inline(always)] + pub fn rotate_right(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + // SAFETY: as `rotate_left` above. + Self(unsafe { _mm512_rorv_epi64(self.0, _mm512_set1_epi64(n as i64)) }) + } + #[inline(always)] pub fn splat(v: u64) -> Self { Self(unsafe { _mm512_set1_epi64(v as i64) }) diff --git a/src/simd_nightly/u_word_types.rs b/src/simd_nightly/u_word_types.rs index 9592102e..93fede04 100644 --- a/src/simd_nightly/u_word_types.rs +++ b/src/simd_nightly/u_word_types.rs @@ -19,6 +19,48 @@ pub struct U64x8(pub u64x8); impl U64x8 { pub const LANES: usize = 8; + /// Lane-wise left-rotate by `n` bits — the u64 ARX rotate (BLAKE2b / + /// argon2 lane). `n` is taken mod 64; `n == 0` returns `self`, since + /// `x >> 64` is UB on `u64`. + /// + /// `core::simd` has no rotate, so this is the same per-lane loop the + /// scalar backend uses. The codegen oracle measured that such a loop does + /// **not** vectorize on stable (0 packed, one `rorq` per lane, across + /// three spellings); whether this backend's codegen does better is + /// **unmeasured** — the oracle runs on stable and cannot see it. The + /// native `VPROLVQ`/`VPRORVQ` override lives on `simd_avx512`'s `U64x8`. + /// See `.claude/knowledge/crypto-lane-status.md`. + #[inline(always)] + pub fn rotate_left(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + let a = self.to_array(); + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = a[i].rotate_left(n); + } + Self::from_array(o) + } + + /// Lane-wise right-rotate by `n` bits — BLAKE2b's direction. + /// `rotr(n) == rotl(64 - n)` exactly; kept distinct because BLAKE2b and + /// argon2 are specified in terms of right rotation. + #[inline(always)] + pub fn rotate_right(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + let a = self.to_array(); + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = a[i].rotate_right(n); + } + Self::from_array(o) + } + #[inline(always)] pub fn splat(v: u64) -> Self { Self(u64x8::splat(v)) diff --git a/src/simd_scalar.rs b/src/simd_scalar.rs index 2e7f09f7..70b0114f 100644 --- a/src/simd_scalar.rs +++ b/src/simd_scalar.rs @@ -526,6 +526,54 @@ impl_int_type!(U16x32, u16, 32, 0u16); impl_int_type!(U32x16, u32, 16, 0u32); impl_int_type!(U64x8, u64, 8, 0u64); +/// u64 ARX rotate — the BLAKE2b / argon2 lane. +/// +/// Scalar per-lane loops, and **measured not to vectorize**: the codegen +/// oracle tried three spellings (`u64::rotate_right`, an explicit shift-or +/// with a runtime amount, and the same with BLAKE2b's constants 32/24/16/63) +/// and every one came back 0 packed, one `rorq` per lane. LLVM declines the +/// 64-bit *operation*, not the rotate *idiom* — it folded two of the probes +/// into byte-identical code. +/// +/// So unlike every other lane-wise op in this crate, the scalar spec is NOT +/// the implementation here. The native `VPROLVQ`/`VPRORVQ` override lives on +/// `simd_avx512`'s `U64x8`, which is a real `__m512i`; these arms are the +/// correct-but-unvectorized fallback, and that is a known cost rather than an +/// oversight. See `.claude/knowledge/crypto-lane-status.md`. +impl U64x8 { + /// Lane-wise left-rotate by `n` bits. `n` is taken mod 64. + #[inline(always)] + pub fn rotate_left(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + let a = self.to_array(); + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = a[i].rotate_left(n); + } + Self::from_array(o) + } + + /// Lane-wise right-rotate by `n` bits — BLAKE2b's direction. + /// `rotr(n) == rotl(64 - n)` exactly; kept distinct because BLAKE2b and + /// argon2 are specified in terms of right rotation. + #[inline(always)] + pub fn rotate_right(self, n: u32) -> Self { + let n = n % 64; + if n == 0 { + return self; + } + let a = self.to_array(); + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = a[i].rotate_right(n); + } + Self::from_array(o) + } +} + // I8/I16 SIMD types (scalar fallback) impl_int_type!(I8x64, i8, 64, 0i8); impl_int_type!(I8x32, i8, 32, 0i8);