Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 73 additions & 20 deletions .claude/CHACHA20_MATRYOSHKA_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,79 @@ CI: `wasm_simd` job (`.github/workflows/ci.yaml` + `scripts/wasm-parity.sh` +
lane-by-lane parity selfcheck under **node** — the standing guard for the wasm
tier (invisible to the x86 `cargo test`). Extend its per-lane blocks as lanes land.

The stand-alone `src/simd_crypto.rs` (`chacha20_block`/`chacha20_keystream`,
scalar+AVX-512+wasm128, RustCrypto-parity-proven) is the *interim* primitive; the
matryoshka below **supersedes it** (RustCrypto supplies the rounds, so the
from-scratch cipher retires once the fork lands).

## DEFERRED — TO-DO (next session)

1. **Native neon `U32x16 = [U32x4; 4]`.** `simd_neon.rs` has native
`U32x4(uint32x4_t)` with add/sub/min/max; add `bitxor` (`veorq_u32`) +
`rotate_left` (shift-or via `vshlq_u32` with variable count vectors, `n%32`
guard), then compose `U32x16([U32x4; 4])` with `impl Add`/`impl BitXor`/
`rotate_left` (fan over 4 lanes, mirror the wasm shape in `simd_wasm.rs`), fix
`F32x16::to_bits/from_bits` to `from_array`/`to_array`, and swap the `simd.rs`
aarch64 arm's scalar `U32x16` re-export for the native one.
2. **aarch64 cross parity CI.** Generalize `wasm-simd-parity` into a shared
`simd-parity` harness (or add a sibling) run under **qemu** via `cross` in a
new `neon_simd` CI job — same selfcheck, closing NEON's identical x86-suite
blind spot (its `F32x16`/`I8x16` are unverified in CI today too).
3. **avx2 native `U32x16`** (2×`__m256i`) — the TD-SIMD-3 lowering; optional, the
scalar polyfill is correct meanwhile.
The stand-alone `src/simd_crypto.rs` was the *interim* primitive; the matryoshka
below **superseded it** — it is now **RETIRED** (2026-07-12, see below).

## ✅ SHIPPED 2026-07-12 — fork landed + `simd_crypto.rs` retired

- **Native NEON `U32x16 = [U32x4; 4]`** — `U32x4` gained `bitxor`(veorq_u32) +
`rotate_left`(vshlq_u32 shift-or); composed `U32x16` with Add/BitXor/
rotate_left. aarch64 now `cargo check`-clean on stable (also fixed the
pre-existing `u16x8` alias + the nightly-only `vdotq_s32` breakage → stable
widening NEON). Every tier of the ARX lane is now native.
- **The fork** lives at `vendor/chacha20/` (name/version kept `chacha20`/`0.9.1`,
excluded from the workspace, own `[workspace]` table). Upstream cipher verbatim;
the ONE delta is `src/backends/ndarray_simd.rs` — the transpose block16 (16
blocks ∥, word→16-lane vector, counter lane-index) over `ndarray::simd::U32x16`,
pure `+`/`^`/`rotate_left`, **no raw intrinsics, no `unsafe`**. Selected at
compile time under `cfg(all(x86_64, avx512f))` in `backends.rs` +
`lib.rs::process_with_backend` (Tokens/new/dispatch — four cfg branches). The
`ndarray` dep is `target.'cfg(target_arch="x86_64")'` + `default-features=false,
features=["std"]` so wasm/aarch64 fork builds never pull ndarray.
- **`[patch.crates-io] chacha20 = { path = "vendor/chacha20" }`** in the ndarray
root `Cargo.toml` folds it under `chacha20poly1305 → encryption` transparently.
- **Parity — triple-gated, GREEN:** the fork's own RustCrypto RFC 8439 vectors
(`chacha20_encryption`/`_keystream`, `xchacha20_*`, `chacha20_core`, seek) pass
**through `ndarray_simd`** under `-Ctarget-cpu=x86-64-v4`; `cargo test -p
encryption` (23 AEAD tests incl. XChaCha20-Poly1305 round-trip / bit-flip /
wrong-key) passes on both the default (v3→RustCrypto avx2 fallback) and avx512
(→`ndarray_simd`) builds.
- **RETIRED:** deleted `src/simd_crypto.rs` + `tests/chacha20_rustcrypto_parity.rs`
+ the `chacha20 = "0.9"` dev-dep; removed the `ndarray::simd::{chacha20_block,
chacha20_keystream, chacha20_state}` surface. RustCrypto owns the cipher; ndarray
owns only the `U32x16` lane. `aead.rs` doc updated to the transitive-acceleration
framing.

### Reality note + remaining follow-ups
- **The workspace default is `target-cpu=x86-64-v3` (AVX2), NOT v4** — the CLAUDE.md
"v4 mandatory" line is stale. So `ndarray_simd` activates only on an **avx512
build** (`.cargo/config-avx512.toml` / `-Ctarget-cpu=x86-64-v4`); the default v3
build uses RustCrypto's own avx2 backend (fast, vetted). This is correct and
safe — the matryoshka is the *server-avx512* accelerator.
- ~~**wasm browser backend.**~~ **DONE 2026-07-12** — the fork's `ndarray_simd`
cfg gates were widened to `any(all(x86_64, avx512f), all(wasm32, simd128))` in
`backends.rs` + `lib.rs::process_with_backend`, and the ndarray dep to
`cfg(any(x86_64, wasm32))`. The SAME `ndarray_simd.rs` source now drives the
AVX-512 `__m512i` lane (server) and the native wasm `[U32x4;4]` lane (browser).
Verified: `cargo build --manifest-path vendor/chacha20/Cargo.toml --target
wasm32-unknown-unknown --lib` with `+simd128` compiles ndarray-for-wasm + the
wasm backend clean; x86 avx512 vectors still 9/9 green after the widening. Bit-
exact by composition (the `ndarray_simd` source is RFC-proven on x86; the wasm
`U32x16` lane is node-proven by the `wasm_simd` CI job). A CI compile-guard step
was added to the `wasm_simd` job. A consumer building `encryption` as a wasm
cdylib with `+simd128` now gets the accelerated keystream transparently.
- **cross-repo `[patch]` (next):** `[patch]` does not transit, so MedCare-rs (and
any other consumer building `encryption` for avx512) needs its own
`[patch.crates-io] chacha20 = { path = "vendor/chacha20" }` pointing at a vendored
copy of the fork, to inherit the acceleration. Documented, low-risk.

## DONE / DEFERRED

1. ~~**Native neon `U32x16 = [U32x4; 4]`.**~~ **DONE 2026-07-12** — `U32x4` gained
`bitxor`(veorq_u32) + `rotate_left`(vshlq_u32 shift-or, n%32 guard); composed
native `U32x16([U32x4;4])` with Add/BitXor/rotate_left; `F32x16::to_bits/
from_bits` → from_array/to_array; `simd.rs` aarch64 arm re-exports the native
lane. (+ fixed the pre-existing aarch64 stable-compile breakage.)
2. ~~**aarch64 cross parity CI.**~~ **DONE 2026-07-12** — `crates/neon-simd-parity`
(excluded bin, real `ndarray::simd` aarch64 types) + `scripts/neon-parity.sh`
(cross-build aarch64 + run under `qemu-aarch64-static`) + CI `neon_simd`
(`neon-simd/parity-qemu`) job, added to the `conclusion` needs. Runtime-verifies
U32x16 ARX (rotate 16/12/8/7 + edges) / F32x16 / I8x16 == scalar. Twin of
`wasm_simd`. **Green locally under qemu.**
3. **avx2 native `U32x16`** (2×`__m256i`) — the TD-SIMD-3 lowering; still optional,
the scalar polyfill is correct meanwhile.
4. **wasm matryoshka backend + cross-repo `[patch]`** — see the SHIPPED section's
follow-ups above.

## The MATRYOSHKA — how the lane gets USED (the finalization)

Expand Down
49 changes: 49 additions & 0 deletions .claude/blackboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,3 +630,52 @@ is superseded by the matryoshka once the fork lands.
4. **Matryoshka execution:** fork `chacha20`, clone `avx2.rs` backend → rewire over
`ndarray::simd::U32x16`, `[patch]` into the encryption stack (transitive accel),
gate vs RustCrypto `soft`, retire `simd_crypto.rs`. Full plan in the doc above.

---

## 2026-07-12 — PR #240 CI fully green (no_std/MSRV fix chain closed)

Tip `5a914c37`. All 20 checks pass; the three previously-red failures are resolved:

- **261f736a** — `simd_crypto` dispatcher: `is_x86_feature_detected!` → compile-time
`#[cfg(all(target_arch="x86_64", target_feature="avx512f"))]` (no runtime detection;
the workspace SIMD-dispatch rule). Unblocked `blas-msrv` + `nostd/thumbv6m`.
- **5a914c37** — three no_std/bare-`--no-default-features` test-build fixes:
- `simd_crypto` tests: `vec![[0u8;64]; n]` → fixed arrays `[[0u8;64]; 40]` / `[[0u8;64]; 16]`
(no `vec!` under no_std).
- `tests/chacha20_rustcrypto_parity.rs`: added `#![cfg(feature = "std")]` (imports
std-gated `ndarray::simd`; no-ops under `--no-default-features`).
- `src/tri.rs`: `Array2::<i32>::zeros(...)` (serde_json dep-drift added
`impl PartialEq<Value> for i32`, making bare `Array2::zeros` element-type ambiguous).

Green jobs of note: `tests/{stable,beta,1.95.0}`, `blas-msrv`, `nostd/thumbv6m-none-eabi`,
`clippy/1.95.0`, `format/stable`, `native-backend/stable`, `tier4-avx512-check`,
`wasm-simd/parity-node` (new gate), `hpc-stream-parallel/rayon`, CodeRabbit.

Deferred (task #30, token-limit call): native neon `U32x16=[U32x4;4]` + aarch64 cross
parity CI + the matryoshka chacha20 fork. Plan in `.claude/CHACHA20_MATRYOSHKA_PLAN.md`.

---

## 2026-07-12 — Matryoshka finalized + NEON cross-CI (deferred #30 CLOSED)

- **Native NEON `U32x16 = [U32x4;4]` ARX lane** (commit 06a61bf9): bitxor/
rotate_left on U32x4, composed U32x16 Add/BitXor/rotate_left; also fixed the
pre-existing aarch64 stable-compile breakage (`u16x8` alias; nightly-only
`vdotq_s32` → stable widening NEON). aarch64 now `cargo check`-clean on stable.
- **ChaCha20 matryoshka + `simd_crypto.rs` RETIRED** (commit 20dc6c3f):
`vendor/chacha20/` fork (name/version kept, own [workspace]); the ONE delta is
`backends/ndarray_simd.rs` — the transpose block16 over `ndarray::simd::U32x16`
(pure +/^/rotate_left, no intrinsics, no unsafe), compile-time-selected under
cfg(x86_64+avx512f), `[patch.crates-io]`-folded under encryption. Triple parity
gate GREEN (fork RFC 8439 vectors through ndarray_simd @ v4; encryption 23 AEAD
tests @ v3+v4). Deleted src/simd_crypto.rs + the parity test + the chacha20 dev-dep
+ the ndarray::simd::chacha20_* surface.
- **NEON cross parity CI**: `crates/neon-simd-parity` (excluded bin) +
`scripts/neon-parity.sh` (cross-build aarch64 + run under qemu-aarch64-static) +
CI `neon_simd` job (added to conclusion needs). Runtime-verifies U32x16 ARX /
F32x16 / I8x16 == scalar on real aarch64. Green locally under qemu.

Follow-ups (documented in `.claude/CHACHA20_MATRYOSHKA_PLAN.md`): wasm matryoshka
backend (simd128 branch); cross-repo `[patch]` for MedCare-rs; the workspace
default is x86-64-v3 (avx2) so ndarray_simd activates on avx512 builds only.
29 changes: 29 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@ jobs:
node-version: "22"
- name: wasm SIMD parity (build + node run)
run: ./scripts/wasm-parity.sh
# Guard the wasm matryoshka: the chacha20 fork's ndarray_simd backend must
# keep compiling for wasm32+simd128 (it rides the same U32x16 lane the node
# parity step above proves bit-exact). A future ndarray::simd change that
# broke the wasm backend build would fail here.
- name: wasm matryoshka backend (chacha20 fork) compiles for wasm32+simd128
run: RUSTFLAGS="-C target-feature=+simd128" cargo build --manifest-path vendor/chacha20/Cargo.toml --target wasm32-unknown-unknown --lib

neon_simd:
# The x86 `cargo test` suite never touches `simd_neon.rs` (it is cfg'd to
# aarch64), so NEON SIMD correctness is otherwise unverified. Cross-build the
# real `ndarray::simd` aarch64 types via the excluded `neon-simd-parity` bin
# and run its numeric selfcheck under qemu — both integration (the lib
# compiles for aarch64 on stable) and parity (each lane == scalar). The
# aarch64 twin of the wasm_simd job above.
runs-on: ubuntu-latest
name: neon-simd/parity-qemu
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
# rust-toolchain.toml pins 1.95.0 — install the aarch64 target for the
# pinned toolchain too (dtolnay installs for `stable`, which may differ).
- run: rustup target add aarch64-unknown-linux-gnu
- name: install aarch64 cross toolchain + qemu-user
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu qemu-user-static
- name: NEON SIMD parity (cross-build + qemu run)
run: ./scripts/neon-parity.sh

tests:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -359,6 +387,7 @@ jobs:
- format
- nostd
- wasm_simd
- neon_simd
- tests
- native-backend
- hpc-stream-parallel
Expand Down
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 13 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,6 @@ approx = { workspace = true, default-features = true }
itertools = { workspace = true }
ndarray-gen = { workspace = true }
criterion = { version = "0.5", features = ["html_reports"] }
# The vetted RustCrypto ChaCha20 stream cipher — used ONLY as the independent
# oracle for the `chacha20_rustcrypto_parity` integration test that pins
# `ndarray::simd::chacha20_keystream` byte-for-byte against a trusted reference.
# Same version chacha20poly1305 0.10 pulls transitively (see Cargo.lock).
chacha20 = "0.9"

[[bench]]
name = "append"
Expand Down Expand Up @@ -407,7 +402,7 @@ members = [
"ndarray-rand",
"crates/*",
]
exclude = ["crates/burn", "crates/wasm-simd-parity"]
exclude = ["crates/burn", "crates/wasm-simd-parity", "crates/neon-simd-parity", "vendor/chacha20"]
default-members = [
".",
"ndarray-rand",
Expand Down Expand Up @@ -449,3 +444,15 @@ tag-name = "{{version}}"
features = ["approx", "serde", "rayon"]
# Define the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]

# ── ChaCha20 matryoshka (AdaWorldAPI fork) ──────────────────────────────────
# Fold the AVX-512/ndarray::simd-accelerated `chacha20` fork over the registry
# crate. This transitively accelerates the `encryption` crate's XChaCha20-Poly1305
# keystream (chacha20poly1305 -> chacha20) with zero change to the AEAD, on any
# x86_64+avx512f build (the workspace's `target-cpu=x86-64-v4`). Non-x86 builds of
# the fork fall through to RustCrypto's own backends and never pull ndarray.
# NOTE: a "Patch `chacha20` was not used in the crate graph" note appears on
# scoped builds that don't pull `encryption` (e.g. `-p ndarray`); it is benign —
# the patch is exercised by `cargo test -p encryption` and the full workspace.
[patch.crates-io]
chacha20 = { path = "vendor/chacha20" }
22 changes: 11 additions & 11 deletions crates/encryption/src/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
//! and a server. Callers normally use [`crate::envelope`], which manages
//! nonce generation and layout; this module is the raw primitive.
//!
//! ## Deliberately NOT hand-composed from `ndarray::simd::chacha20_keystream`
//! ## Acceleration is transitive (the matryoshka), never hand-composed
//!
//! `ndarray` now ships a hardware-accelerated ChaCha20 keystream
//! (`ndarray::simd::chacha20_keystream`, AVX-512 / wasm128, byte-parity-proven
//! against RustCrypto). It is tempting to swap this AEAD's keystream to that
//! primitive for speed — **do not.** XChaCha20-Poly1305 is not just a keystream:
//! it is HChaCha20 subkey derivation + the Poly1305 one-time-key + MAC framing +
//! the AAD/length encoding. Re-wiring only the keystream means re-implementing
//! that authenticated composition by hand, which is exactly the "roll your own
//! AEAD" footgun the stack forbids. The vetted RustCrypto `XChaCha20Poly1305`
//! construction stays here. The accelerated primitive is for *raw-stream* use
//! sites whose caller already owns a vetted MAC/framing — not this module.
//! This AEAD stays 100% vetted RustCrypto `XChaCha20Poly1305` — HChaCha20 subkey
//! derivation + Poly1305 one-time-key + MAC framing + AAD/length encoding — and
//! we do **not** hand-wire a raw keystream into it (that would be the "roll your
//! own AEAD" footgun the stack forbids). Hardware acceleration instead arrives
//! *underneath*, transparently: the AdaWorldAPI `chacha20` fork (`vendor/chacha20/`)
//! replaces one backend of the transitive `chacha20` dependency with a keystream
//! double-round expressed over `ndarray::simd::U32x16` (the AVX-512 lane), folded
//! in via `[patch.crates-io]`. So `chacha20poly1305 -> chacha20 -> ndarray::simd`
//! accelerates on an AVX-512 build with zero change to this module, and every
//! RustCrypto RFC 8439 test vector (incl. XChaCha20) still passes through it.

use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
Expand Down
2 changes: 2 additions & 0 deletions crates/neon-simd-parity/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
29 changes: 29 additions & 0 deletions crates/neon-simd-parity/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# neon-simd-parity — the run-under-qemu gate for the aarch64 NEON SIMD tier.
#
# The x86 `cargo test` suite never touches `simd_neon.rs` (it is `cfg`'d to
# aarch64), so NEON SIMD correctness is otherwise unverified by CI — exactly the
# blind spot `wasm-simd-parity` closes for wasm. This bin calls the REAL
# `ndarray::simd` aarch64 types (no copy → no drift) and self-checks each lane's
# arithmetic against scalar in-process; it is cross-built for
# `aarch64-unknown-linux-gnu` and executed under `qemu-aarch64-static`, exiting
# non-zero on any lane mismatch.
#
# EXCLUDED from the workspace (see root Cargo.toml `exclude`) so it has zero
# effect on the x86 jobs; the CI `neon_simd` job builds it via --manifest-path.
[package]
name = "neon-simd-parity"
version = "0.0.0"
edition = "2021"
publish = false

[[bin]]
name = "neon-simd-parity"
path = "src/main.rs"

[dependencies]
# `simd` needs only ndarray's `std` feature, not the full HPC default set.
ndarray = { path = "../..", default-features = false, features = ["std"] }

[profile.release]
panic = "abort"
opt-level = "z"
Loading
Loading