Skip to main content

logicaffeine_system/
keccak.rs

1//! Keccak-f\[1600\] + SHA-3 / SHAKE runtime kernels — the symmetric/hash layer ML-KEM is built on
2//! (SHA3-256/512 for hashing, SHAKE128 for matrix expansion, SHAKE256 for the PRF). Reached from
3//! compiled LOGOS via the `sha3_256` / `sha3_512` / `shake128` / `shake256` stdlib functions.
4//! Verified against the NIST FIPS-202 KATs (see the tests). Input/output are LOGOS `Int` bytes
5//! (0..255) carried in a `Seq of Int`.
6
7const RC: [u64; 24] = [
8    0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000,
9    0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
10    0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a,
11    0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003,
12    0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a,
13    0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008,
14];
15/// The Keccak-f\[1600\] permutation: 24 rounds of θ, ρ·π, χ, ι over the 5×5×64 state.
16pub fn keccak_f1600(st: &mut [u64; 25]) {
17    // Fully unrolled into 25 register-resident lanes — no array indexing, bounds checks, or PILN/
18    // ROTC lookups in the hot loop (the proven-fast scalar form). θ, ρ·π (combined into the `b`
19    // lanes), χ, ι. Bit-identical to the table-driven reference (NIST FIPS-202 KAT-verified).
20    let [mut a0, mut a1, mut a2, mut a3, mut a4, mut a5, mut a6, mut a7, mut a8, mut a9, mut a10, mut a11, mut a12, mut a13, mut a14, mut a15, mut a16, mut a17, mut a18, mut a19, mut a20, mut a21, mut a22, mut a23, mut a24] =
21        *st;
22    for &rc in RC.iter() {
23        // θ
24        let c0 = a0 ^ a5 ^ a10 ^ a15 ^ a20;
25        let c1 = a1 ^ a6 ^ a11 ^ a16 ^ a21;
26        let c2 = a2 ^ a7 ^ a12 ^ a17 ^ a22;
27        let c3 = a3 ^ a8 ^ a13 ^ a18 ^ a23;
28        let c4 = a4 ^ a9 ^ a14 ^ a19 ^ a24;
29        let d0 = c4 ^ c1.rotate_left(1);
30        let d1 = c0 ^ c2.rotate_left(1);
31        let d2 = c1 ^ c3.rotate_left(1);
32        let d3 = c2 ^ c4.rotate_left(1);
33        let d4 = c3 ^ c0.rotate_left(1);
34        a0 ^= d0; a5 ^= d0; a10 ^= d0; a15 ^= d0; a20 ^= d0;
35        a1 ^= d1; a6 ^= d1; a11 ^= d1; a16 ^= d1; a21 ^= d1;
36        a2 ^= d2; a7 ^= d2; a12 ^= d2; a17 ^= d2; a22 ^= d2;
37        a3 ^= d3; a8 ^= d3; a13 ^= d3; a18 ^= d3; a23 ^= d3;
38        a4 ^= d4; a9 ^= d4; a14 ^= d4; a19 ^= d4; a24 ^= d4;
39        // ρ + π: b[π(i)] = rotl(a[i], ρ[i])
40        let b0 = a0;
41        let b1 = a6.rotate_left(44);
42        let b2 = a12.rotate_left(43);
43        let b3 = a18.rotate_left(21);
44        let b4 = a24.rotate_left(14);
45        let b5 = a3.rotate_left(28);
46        let b6 = a9.rotate_left(20);
47        let b7 = a10.rotate_left(3);
48        let b8 = a16.rotate_left(45);
49        let b9 = a22.rotate_left(61);
50        let b10 = a1.rotate_left(1);
51        let b11 = a7.rotate_left(6);
52        let b12 = a13.rotate_left(25);
53        let b13 = a19.rotate_left(8);
54        let b14 = a20.rotate_left(18);
55        let b15 = a4.rotate_left(27);
56        let b16 = a5.rotate_left(36);
57        let b17 = a11.rotate_left(10);
58        let b18 = a17.rotate_left(15);
59        let b19 = a23.rotate_left(56);
60        let b20 = a2.rotate_left(62);
61        let b21 = a8.rotate_left(55);
62        let b22 = a14.rotate_left(39);
63        let b23 = a15.rotate_left(41);
64        let b24 = a21.rotate_left(2);
65        // χ (row-wise: a[i] = b[i] ^ (¬b[i+1] ∧ b[i+2]))
66        a0 = b0 ^ (!b1 & b2); a1 = b1 ^ (!b2 & b3); a2 = b2 ^ (!b3 & b4); a3 = b3 ^ (!b4 & b0); a4 = b4 ^ (!b0 & b1);
67        a5 = b5 ^ (!b6 & b7); a6 = b6 ^ (!b7 & b8); a7 = b7 ^ (!b8 & b9); a8 = b8 ^ (!b9 & b5); a9 = b9 ^ (!b5 & b6);
68        a10 = b10 ^ (!b11 & b12); a11 = b11 ^ (!b12 & b13); a12 = b12 ^ (!b13 & b14); a13 = b13 ^ (!b14 & b10); a14 = b14 ^ (!b10 & b11);
69        a15 = b15 ^ (!b16 & b17); a16 = b16 ^ (!b17 & b18); a17 = b17 ^ (!b18 & b19); a18 = b18 ^ (!b19 & b15); a19 = b19 ^ (!b15 & b16);
70        a20 = b20 ^ (!b21 & b22); a21 = b21 ^ (!b22 & b23); a22 = b22 ^ (!b23 & b24); a23 = b23 ^ (!b24 & b20); a24 = b24 ^ (!b20 & b21);
71        // ι
72        a0 ^= rc;
73    }
74    *st = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24];
75}
76
77/// Rotate-left 4 lanes (one per packed Keccak state) by `n ∈ [1, 63]`. AVX2 has no 64-bit rotate
78/// before AVX-512, so it is the `(x << n) | (x >> (64−n))` pair with variable-count shifts.
79#[cfg(target_arch = "x86_64")]
80#[target_feature(enable = "avx2")]
81#[inline]
82unsafe fn rotl64_x4(x: std::arch::x86_64::__m256i, n: i32) -> std::arch::x86_64::__m256i {
83    use std::arch::x86_64::*;
84    _mm256_or_si256(
85        _mm256_sll_epi64(x, _mm_cvtsi32_si128(n)),
86        _mm256_srl_epi64(x, _mm_cvtsi32_si128(64 - n)),
87    )
88}
89
90/// Keccak-f[1600] over **four independent states at once** — lane `i` of each `__m256i` is lane `i`
91/// of state `i`. The same θ / ρ·π / χ / ι as the scalar `keccak_f1600`, with AVX2 lane-parallel XOR,
92/// rotate, and `andnot` (`χ`'s `¬b ∧ c`). Bit-identical, per lane, to four scalar permutations.
93#[cfg(target_arch = "x86_64")]
94#[target_feature(enable = "avx2")]
95pub(crate) unsafe fn keccak_f1600_x4(st: &mut [std::arch::x86_64::__m256i; 25]) {
96    use std::arch::x86_64::*;
97    macro_rules! xor {
98        ($a:expr, $b:expr) => {
99            _mm256_xor_si256($a, $b)
100        };
101    }
102    let mut a = *st;
103    for &rc in RC.iter() {
104        // θ
105        let c0 = xor!(xor!(xor!(xor!(a[0], a[5]), a[10]), a[15]), a[20]);
106        let c1 = xor!(xor!(xor!(xor!(a[1], a[6]), a[11]), a[16]), a[21]);
107        let c2 = xor!(xor!(xor!(xor!(a[2], a[7]), a[12]), a[17]), a[22]);
108        let c3 = xor!(xor!(xor!(xor!(a[3], a[8]), a[13]), a[18]), a[23]);
109        let c4 = xor!(xor!(xor!(xor!(a[4], a[9]), a[14]), a[19]), a[24]);
110        let d0 = xor!(c4, rotl64_x4(c1, 1));
111        let d1 = xor!(c0, rotl64_x4(c2, 1));
112        let d2 = xor!(c1, rotl64_x4(c3, 1));
113        let d3 = xor!(c2, rotl64_x4(c4, 1));
114        let d4 = xor!(c3, rotl64_x4(c0, 1));
115        a[0] = xor!(a[0], d0); a[5] = xor!(a[5], d0); a[10] = xor!(a[10], d0); a[15] = xor!(a[15], d0); a[20] = xor!(a[20], d0);
116        a[1] = xor!(a[1], d1); a[6] = xor!(a[6], d1); a[11] = xor!(a[11], d1); a[16] = xor!(a[16], d1); a[21] = xor!(a[21], d1);
117        a[2] = xor!(a[2], d2); a[7] = xor!(a[7], d2); a[12] = xor!(a[12], d2); a[17] = xor!(a[17], d2); a[22] = xor!(a[22], d2);
118        a[3] = xor!(a[3], d3); a[8] = xor!(a[8], d3); a[13] = xor!(a[13], d3); a[18] = xor!(a[18], d3); a[23] = xor!(a[23], d3);
119        a[4] = xor!(a[4], d4); a[9] = xor!(a[9], d4); a[14] = xor!(a[14], d4); a[19] = xor!(a[19], d4); a[24] = xor!(a[24], d4);
120        // ρ + π
121        let b0 = a[0];
122        let b1 = rotl64_x4(a[6], 44);
123        let b2 = rotl64_x4(a[12], 43);
124        let b3 = rotl64_x4(a[18], 21);
125        let b4 = rotl64_x4(a[24], 14);
126        let b5 = rotl64_x4(a[3], 28);
127        let b6 = rotl64_x4(a[9], 20);
128        let b7 = rotl64_x4(a[10], 3);
129        let b8 = rotl64_x4(a[16], 45);
130        let b9 = rotl64_x4(a[22], 61);
131        let b10 = rotl64_x4(a[1], 1);
132        let b11 = rotl64_x4(a[7], 6);
133        let b12 = rotl64_x4(a[13], 25);
134        let b13 = rotl64_x4(a[19], 8);
135        let b14 = rotl64_x4(a[20], 18);
136        let b15 = rotl64_x4(a[4], 27);
137        let b16 = rotl64_x4(a[5], 36);
138        let b17 = rotl64_x4(a[11], 10);
139        let b18 = rotl64_x4(a[17], 15);
140        let b19 = rotl64_x4(a[23], 56);
141        let b20 = rotl64_x4(a[2], 62);
142        let b21 = rotl64_x4(a[8], 55);
143        let b22 = rotl64_x4(a[14], 39);
144        let b23 = rotl64_x4(a[15], 41);
145        let b24 = rotl64_x4(a[21], 2);
146        // χ (¬b[i+1] ∧ b[i+2] is exactly _mm256_andnot_si256)
147        a[0] = xor!(b0, _mm256_andnot_si256(b1, b2)); a[1] = xor!(b1, _mm256_andnot_si256(b2, b3)); a[2] = xor!(b2, _mm256_andnot_si256(b3, b4)); a[3] = xor!(b3, _mm256_andnot_si256(b4, b0)); a[4] = xor!(b4, _mm256_andnot_si256(b0, b1));
148        a[5] = xor!(b5, _mm256_andnot_si256(b6, b7)); a[6] = xor!(b6, _mm256_andnot_si256(b7, b8)); a[7] = xor!(b7, _mm256_andnot_si256(b8, b9)); a[8] = xor!(b8, _mm256_andnot_si256(b9, b5)); a[9] = xor!(b9, _mm256_andnot_si256(b5, b6));
149        a[10] = xor!(b10, _mm256_andnot_si256(b11, b12)); a[11] = xor!(b11, _mm256_andnot_si256(b12, b13)); a[12] = xor!(b12, _mm256_andnot_si256(b13, b14)); a[13] = xor!(b13, _mm256_andnot_si256(b14, b10)); a[14] = xor!(b14, _mm256_andnot_si256(b10, b11));
150        a[15] = xor!(b15, _mm256_andnot_si256(b16, b17)); a[16] = xor!(b16, _mm256_andnot_si256(b17, b18)); a[17] = xor!(b17, _mm256_andnot_si256(b18, b19)); a[18] = xor!(b18, _mm256_andnot_si256(b19, b15)); a[19] = xor!(b19, _mm256_andnot_si256(b15, b16));
151        a[20] = xor!(b20, _mm256_andnot_si256(b21, b22)); a[21] = xor!(b21, _mm256_andnot_si256(b22, b23)); a[22] = xor!(b22, _mm256_andnot_si256(b23, b24)); a[23] = xor!(b23, _mm256_andnot_si256(b24, b20)); a[24] = xor!(b24, _mm256_andnot_si256(b20, b21));
152        // ι
153        a[0] = xor!(a[0], _mm256_set1_epi64x(rc as i64));
154    }
155    *st = a;
156}
157
158/// 4-way SHAKE128 absorb of four already-padded single rate-blocks (168 bytes each: the caller
159/// writes the message, the `0x1f` delimiter, and the `0x80` final-bit). Returns the packed state
160/// after the permutation, so its first 168 bytes (per lane) are squeeze block 0 — the lane-parallel
161/// twin of `shake128_absorb` for the 34-byte matrix-expansion XOF seed.
162#[cfg(target_arch = "x86_64")]
163#[target_feature(enable = "avx2")]
164pub(crate) unsafe fn shake128_x4_absorb_once(
165    blocks: &[[u8; 168]; 4],
166) -> [std::arch::x86_64::__m256i; 25] {
167    use std::arch::x86_64::*;
168    let mut st = [_mm256_setzero_si256(); 25];
169    for (lane, slot) in st.iter_mut().enumerate().take(21) {
170        let l = |s: usize| -> i64 {
171            i64::from_le_bytes(blocks[s][lane * 8..lane * 8 + 8].try_into().unwrap())
172        };
173        *slot = _mm256_set_epi64x(l(3), l(2), l(1), l(0));
174    }
175    keccak_f1600_x4(&mut st);
176    st
177}
178
179/// Extract the current 168-byte rate block of each of the four packed SHAKE128 states. Call
180/// `keccak_f1600_x4` between blocks to advance the squeeze.
181#[cfg(target_arch = "x86_64")]
182#[target_feature(enable = "avx2")]
183pub(crate) unsafe fn shake128_x4_squeeze_block(
184    st: &[std::arch::x86_64::__m256i; 25],
185) -> [[u8; 168]; 4] {
186    use std::arch::x86_64::*;
187    let mut out = [[0u8; 168]; 4];
188    let mut lane_bytes = [0u64; 4];
189    for (lane, slot) in st.iter().enumerate().take(21) {
190        _mm256_storeu_si256(lane_bytes.as_mut_ptr() as *mut __m256i, *slot);
191        for (state, &v) in lane_bytes.iter().enumerate() {
192            out[state][lane * 8..lane * 8 + 8].copy_from_slice(&v.to_le_bytes());
193        }
194    }
195    out
196}
197
198/// 4-way SHAKE256 PRF for ML-KEM's CBD noise: absorb four independent inputs (each `seed‖nonce`,
199/// ≤135 bytes → a single rate block) and squeeze 128 bytes from each — the lane-parallel twin of a
200/// scalar `SHAKE256(·, 128)`. rate = 136 (17 lanes); 128 output bytes are the first 16 state lanes,
201/// so one `keccak_f1600_x4` suffices (no second squeeze block). Batches the six/seven independent
202/// noise streams of K-PKE keygen/encrypt four-per-permutation, matching the hand-tuned reference libs.
203#[cfg(target_arch = "x86_64")]
204#[target_feature(enable = "avx2")]
205pub(crate) unsafe fn shake256_x4_128(inputs: &[&[u8]; 4]) -> [[u8; 128]; 4] {
206    use std::arch::x86_64::*;
207    const RATE: usize = 136;
208    let mut blocks = [[0u8; RATE]; 4];
209    for (b, inp) in blocks.iter_mut().zip(inputs.iter()) {
210        debug_assert!(inp.len() < RATE, "shake256_x4_128 expects single-block inputs");
211        b[..inp.len()].copy_from_slice(inp);
212        b[inp.len()] = 0x1f;
213        b[RATE - 1] |= 0x80;
214    }
215    let mut st = [_mm256_setzero_si256(); 25];
216    for (lane, slot) in st.iter_mut().enumerate().take(RATE / 8) {
217        let l = |s: usize| -> i64 {
218            i64::from_le_bytes(blocks[s][lane * 8..lane * 8 + 8].try_into().unwrap())
219        };
220        *slot = _mm256_set_epi64x(l(3), l(2), l(1), l(0));
221    }
222    keccak_f1600_x4(&mut st);
223    let mut out = [[0u8; 128]; 4];
224    let mut lane_bytes = [0u64; 4];
225    for (lane, slot) in st.iter().enumerate().take(16) {
226        _mm256_storeu_si256(lane_bytes.as_mut_ptr() as *mut __m256i, *slot);
227        for (state, &v) in lane_bytes.iter().enumerate() {
228            out[state][lane * 8..lane * 8 + 8].copy_from_slice(&v.to_le_bytes());
229        }
230    }
231    out
232}
233
234/// SHAKE128 incremental absorb: XOR `input` into a fresh state at rate 168 with the SHAKE
235/// delimiter and run the final permutation, so the state's first 168 bytes are squeeze block 0.
236/// The caller reads that block, then `keccak_f1600` advances to the next — streaming, no heap.
237pub(crate) fn shake128_absorb(input: &[u8]) -> [u64; 25] {
238    const RATE: usize = 168;
239    const LANES: usize = RATE / 8;
240    let mut st = [0u64; 25];
241    let mut chunks = input.chunks_exact(RATE);
242    for block in &mut chunks {
243        for i in 0..LANES {
244            st[i] ^= u64::from_le_bytes(block[i * 8..i * 8 + 8].try_into().unwrap());
245        }
246        keccak_f1600(&mut st);
247    }
248    let rem = chunks.remainder();
249    let mut block = [0u8; 200];
250    block[..rem.len()].copy_from_slice(rem);
251    block[rem.len()] = 0x1f;
252    block[RATE - 1] |= 0x80;
253    for i in 0..LANES {
254        st[i] ^= u64::from_le_bytes(block[i * 8..i * 8 + 8].try_into().unwrap());
255    }
256    keccak_f1600(&mut st);
257    st
258}
259
260/// SHAKE streaming absorb at an arbitrary `rate` (168 for SHAKE128, 136 for SHAKE256), returning the
261/// state after the final permutation: its first `rate` bytes are squeeze block 0, and `keccak_f1600`
262/// advances to the next block. The caller drives the squeeze (the rejection samplers in `mldsa`).
263pub(crate) fn shake_absorb(input: &[u8], rate: usize) -> [u64; 25] {
264    let lanes = rate / 8;
265    let mut st = [0u64; 25];
266    let mut chunks = input.chunks_exact(rate);
267    for block in &mut chunks {
268        for (i, lane) in st.iter_mut().enumerate().take(lanes) {
269            *lane ^= u64::from_le_bytes(block[i * 8..i * 8 + 8].try_into().unwrap());
270        }
271        keccak_f1600(&mut st);
272    }
273    let rem = chunks.remainder();
274    let mut block = [0u8; 200];
275    block[..rem.len()].copy_from_slice(rem);
276    block[rem.len()] = 0x1f;
277    block[rate - 1] |= 0x80;
278    for (i, lane) in st.iter_mut().enumerate().take(lanes) {
279        *lane ^= u64::from_le_bytes(block[i * 8..i * 8 + 8].try_into().unwrap());
280    }
281    keccak_f1600(&mut st);
282    st
283}
284
285/// Squeeze the current `rate`-byte block from a SHAKE state into `out`.
286pub(crate) fn shake_squeeze_block(st: &[u64; 25], out: &mut [u8], rate: usize) {
287    for i in 0..rate / 8 {
288        out[i * 8..i * 8 + 8].copy_from_slice(&st[i].to_le_bytes());
289    }
290}
291
292/// Advance a SHAKE squeeze to the next block.
293pub(crate) fn keccak_permute(st: &mut [u64; 25]) {
294    keccak_f1600(st);
295}
296
297/// The Keccak sponge: absorb `input` at the given `rate` (bytes) with domain `delim`, then squeeze
298/// `out.len()` bytes. Covers SHA-3 (`delim=0x06`) and SHAKE (`delim=0x1f`).
299fn keccak(rate: usize, delim: u8, input: &[u8], out: &mut [u8]) {
300    let mut st = [0u64; 25];
301    let lanes = rate / 8;
302
303    // Absorb full blocks.
304    let mut chunks = input.chunks_exact(rate);
305    for block in &mut chunks {
306        for i in 0..lanes {
307            st[i] ^= u64::from_le_bytes(block[i * 8..i * 8 + 8].try_into().unwrap());
308        }
309        keccak_f1600(&mut st);
310    }
311    // Pad + absorb the final (partial) block.
312    let rem = chunks.remainder();
313    let mut block = [0u8; 200];
314    block[..rem.len()].copy_from_slice(rem);
315    block[rem.len()] = delim;
316    block[rate - 1] |= 0x80;
317    for i in 0..lanes {
318        st[i] ^= u64::from_le_bytes(block[i * 8..i * 8 + 8].try_into().unwrap());
319    }
320    keccak_f1600(&mut st);
321
322    // Squeeze.
323    let mut off = 0;
324    while off < out.len() {
325        let n = (out.len() - off).min(rate);
326        let mut produced = [0u8; 200];
327        for i in 0..lanes {
328            produced[i * 8..i * 8 + 8].copy_from_slice(&st[i].to_le_bytes());
329        }
330        out[off..off + n].copy_from_slice(&produced[..n]);
331        off += n;
332        if off < out.len() {
333            keccak_f1600(&mut st);
334        }
335    }
336}
337
338pub fn sha3_256_bytes(input: &[u8]) -> [u8; 32] {
339    let mut out = [0u8; 32];
340    keccak(136, 0x06, input, &mut out);
341    out
342}
343pub fn sha3_512_bytes(input: &[u8]) -> [u8; 64] {
344    let mut out = [0u8; 64];
345    keccak(72, 0x06, input, &mut out);
346    out
347}
348pub fn shake128_bytes(input: &[u8], outlen: usize) -> Vec<u8> {
349    let mut out = vec![0u8; outlen];
350    keccak(168, 0x1f, input, &mut out);
351    out
352}
353pub fn shake256_bytes(input: &[u8], outlen: usize) -> Vec<u8> {
354    let mut out = vec![0u8; outlen];
355    keccak(136, 0x1f, input, &mut out);
356    out
357}
358
359// ── LOGOS-facing wrappers (Seq of Int bytes 0..255) ──────────────────────────────────────────
360
361fn seq_to_bytes(s: &[i64]) -> Vec<u8> {
362    s.iter().map(|&x| x.rem_euclid(256) as u8).collect()
363}
364fn bytes_to_seq(b: &[u8]) -> logicaffeine_data::LogosSeq<i64> {
365    logicaffeine_data::LogosSeq::from_vec(b.iter().map(|&x| x as i64).collect())
366}
367
368/// SHA3-256 — the compiled form of LOGOS `sha3_256(a)`. 32 output bytes.
369pub fn sha3_256(input: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
370    bytes_to_seq(&sha3_256_bytes(&seq_to_bytes(input)))
371}
372/// SHA3-512 — 64 output bytes.
373pub fn sha3_512(input: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
374    bytes_to_seq(&sha3_512_bytes(&seq_to_bytes(input)))
375}
376/// SHAKE128 XOF — `outlen` output bytes.
377pub fn shake128(
378    input: &[i64],
379    outlen: i64,
380) -> logicaffeine_data::LogosSeq<i64> {
381    bytes_to_seq(&shake128_bytes(&seq_to_bytes(input), outlen.max(0) as usize))
382}
383/// SHAKE256 XOF — `outlen` output bytes.
384pub fn shake256(
385    input: &[i64],
386    outlen: i64,
387) -> logicaffeine_data::LogosSeq<i64> {
388    bytes_to_seq(&shake256_bytes(&seq_to_bytes(input), outlen.max(0) as usize))
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    fn hex(b: &[u8]) -> String {
396        b.iter().map(|x| format!("{x:02x}")).collect()
397    }
398
399    #[test]
400    #[cfg(target_arch = "x86_64")]
401    fn shake256_x4_128_matches_four_scalar() {
402        if !std::is_x86_feature_detected!("avx2") {
403            return;
404        }
405        // Four independent `seed‖nonce` inputs (the ML-KEM CBD PRF shape) + a couple odd lengths.
406        let inputs: [Vec<u8>; 4] = [
407            (0..33u8).collect(),
408            b"logos ml-kem noise stream one".to_vec(),
409            vec![0xABu8; 100],
410            (0..135u8).map(|i| i.wrapping_mul(7)).collect(),
411        ];
412        let refs: [&[u8]; 4] = [&inputs[0], &inputs[1], &inputs[2], &inputs[3]];
413        let got = unsafe { shake256_x4_128(&refs) };
414        for (lane, inp) in refs.iter().enumerate() {
415            let want = shake256_bytes(inp, 128);
416            assert_eq!(&got[lane][..], &want[..], "shake256_x4 lane {lane} must equal scalar SHAKE256");
417        }
418    }
419
420    #[test]
421    fn sha3_matches_nist_kats() {
422        // FIPS-202 / NIST CAVP vectors.
423        assert_eq!(
424            hex(&sha3_256_bytes(b"")),
425            "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"
426        );
427        assert_eq!(
428            hex(&sha3_256_bytes(b"abc")),
429            "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"
430        );
431        assert_eq!(
432            hex(&sha3_512_bytes(b"")),
433            "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
434        );
435        assert_eq!(
436            hex(&sha3_512_bytes(b"abc")),
437            "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0"
438        );
439    }
440
441    #[test]
442    fn shake_matches_nist_kats() {
443        assert_eq!(
444            hex(&shake128_bytes(b"", 32)),
445            "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26"
446        );
447        assert_eq!(
448            hex(&shake256_bytes(b"", 32)),
449            "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762fb5cb7df11ff5f88f60c5c7e1eb2e83d7ea4f81b7"[..64].to_string()
450        );
451        // SHAKE long output crosses a rate boundary (>168 bytes): exercises multi-block squeeze.
452        let long = shake128_bytes(b"abc", 200);
453        assert_eq!(long.len(), 200);
454        assert_eq!(&hex(&long)[..32], "5881092dd818bf5cf8a3ddb793fbcba7");
455    }
456
457    #[test]
458    #[cfg(target_arch = "x86_64")]
459    fn keccak_f1600_x4_matches_four_scalar_permutations() {
460        use std::arch::x86_64::*;
461        if !std::is_x86_feature_detected!("avx2") {
462            return;
463        }
464        let mut s = 0x9E3779B97F4A7C15u64;
465        let mut rng = || {
466            s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
467            s
468        };
469        for _ in 0..200 {
470            // Four independent random states.
471            let mut states: [[u64; 25]; 4] = [[0u64; 25]; 4];
472            for st in states.iter_mut() {
473                for lane in st.iter_mut() {
474                    *lane = rng();
475                }
476            }
477            // Scalar reference.
478            let mut want = states;
479            for st in want.iter_mut() {
480                keccak_f1600(st);
481            }
482            // Pack lane-parallel: vec[lane] = {state0[lane], .., state3[lane]}.
483            let mut packed = [unsafe { _mm256_setzero_si256() }; 25];
484            for (lane, slot) in packed.iter_mut().enumerate() {
485                *slot = unsafe {
486                    _mm256_set_epi64x(
487                        states[3][lane] as i64,
488                        states[2][lane] as i64,
489                        states[1][lane] as i64,
490                        states[0][lane] as i64,
491                    )
492                };
493            }
494            unsafe { keccak_f1600_x4(&mut packed) };
495            // Unpack and compare per state.
496            for (lane, slot) in packed.iter().enumerate() {
497                let mut out = [0u64; 4];
498                unsafe { _mm256_storeu_si256(out.as_mut_ptr() as *mut __m256i, *slot) };
499                for state in 0..4 {
500                    assert_eq!(
501                        out[state], want[state][lane],
502                        "4-way Keccak lane {lane} of state {state} must equal the scalar permutation"
503                    );
504                }
505            }
506        }
507    }
508
509    #[test]
510    fn logos_wrappers_round_trip_bytes() {
511        let input: Vec<i64> = b"abc".iter().map(|&x| x as i64).collect();
512        let out = sha3_256(&input);
513        let bytes: Vec<u8> = out.borrow().iter().map(|&x| x as u8).collect();
514        assert_eq!(hex(&bytes), "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532");
515    }
516}
517
518#[cfg(all(test, target_arch = "x86_64"))]
519mod x4_lanes_ab {
520    use super::*;
521    use logicaffeine_base::Lanes4Word64 as L;
522
523    /// 4-way Keccak-f[1600] written on the `Lanes4Word64` newtype ops (`bitxor`/`rotl`/`andnot`/`splat`)
524    /// — the exact form Logos `crypto.lg` would compile to. Fully unrolled; θ/ρ·π/χ/ι over 4 states.
525    fn keccak_f1600_x4_lanes(a: &mut [L; 25]) {
526        for &rc in RC.iter() {
527            let c0 = a[0].bitxor(a[5]).bitxor(a[10]).bitxor(a[15]).bitxor(a[20]);
528            let c1 = a[1].bitxor(a[6]).bitxor(a[11]).bitxor(a[16]).bitxor(a[21]);
529            let c2 = a[2].bitxor(a[7]).bitxor(a[12]).bitxor(a[17]).bitxor(a[22]);
530            let c3 = a[3].bitxor(a[8]).bitxor(a[13]).bitxor(a[18]).bitxor(a[23]);
531            let c4 = a[4].bitxor(a[9]).bitxor(a[14]).bitxor(a[19]).bitxor(a[24]);
532            let d0 = c4.bitxor(c1.rotl(1));
533            let d1 = c0.bitxor(c2.rotl(1));
534            let d2 = c1.bitxor(c3.rotl(1));
535            let d3 = c2.bitxor(c4.rotl(1));
536            let d4 = c3.bitxor(c0.rotl(1));
537            a[0] = a[0].bitxor(d0); a[5] = a[5].bitxor(d0); a[10] = a[10].bitxor(d0); a[15] = a[15].bitxor(d0); a[20] = a[20].bitxor(d0);
538            a[1] = a[1].bitxor(d1); a[6] = a[6].bitxor(d1); a[11] = a[11].bitxor(d1); a[16] = a[16].bitxor(d1); a[21] = a[21].bitxor(d1);
539            a[2] = a[2].bitxor(d2); a[7] = a[7].bitxor(d2); a[12] = a[12].bitxor(d2); a[17] = a[17].bitxor(d2); a[22] = a[22].bitxor(d2);
540            a[3] = a[3].bitxor(d3); a[8] = a[8].bitxor(d3); a[13] = a[13].bitxor(d3); a[18] = a[18].bitxor(d3); a[23] = a[23].bitxor(d3);
541            a[4] = a[4].bitxor(d4); a[9] = a[9].bitxor(d4); a[14] = a[14].bitxor(d4); a[19] = a[19].bitxor(d4); a[24] = a[24].bitxor(d4);
542            let b0 = a[0];
543            let b1 = a[6].rotl(44); let b2 = a[12].rotl(43); let b3 = a[18].rotl(21); let b4 = a[24].rotl(14);
544            let b5 = a[3].rotl(28); let b6 = a[9].rotl(20); let b7 = a[10].rotl(3); let b8 = a[16].rotl(45); let b9 = a[22].rotl(61);
545            let b10 = a[1].rotl(1); let b11 = a[7].rotl(6); let b12 = a[13].rotl(25); let b13 = a[19].rotl(8); let b14 = a[20].rotl(18);
546            let b15 = a[4].rotl(27); let b16 = a[5].rotl(36); let b17 = a[11].rotl(10); let b18 = a[17].rotl(15); let b19 = a[23].rotl(56);
547            let b20 = a[2].rotl(62); let b21 = a[8].rotl(55); let b22 = a[14].rotl(39); let b23 = a[15].rotl(41); let b24 = a[21].rotl(2);
548            a[0] = b0.bitxor(b1.andnot(b2)); a[1] = b1.bitxor(b2.andnot(b3)); a[2] = b2.bitxor(b3.andnot(b4)); a[3] = b3.bitxor(b4.andnot(b0)); a[4] = b4.bitxor(b0.andnot(b1));
549            a[5] = b5.bitxor(b6.andnot(b7)); a[6] = b6.bitxor(b7.andnot(b8)); a[7] = b7.bitxor(b8.andnot(b9)); a[8] = b8.bitxor(b9.andnot(b5)); a[9] = b9.bitxor(b5.andnot(b6));
550            a[10] = b10.bitxor(b11.andnot(b12)); a[11] = b11.bitxor(b12.andnot(b13)); a[12] = b12.bitxor(b13.andnot(b14)); a[13] = b13.bitxor(b14.andnot(b10)); a[14] = b14.bitxor(b10.andnot(b11));
551            a[15] = b15.bitxor(b16.andnot(b17)); a[16] = b16.bitxor(b17.andnot(b18)); a[17] = b17.bitxor(b18.andnot(b19)); a[18] = b18.bitxor(b19.andnot(b15)); a[19] = b19.bitxor(b15.andnot(b16));
552            a[20] = b20.bitxor(b21.andnot(b22)); a[21] = b21.bitxor(b22.andnot(b23)); a[22] = b22.bitxor(b23.andnot(b24)); a[23] = b23.bitxor(b24.andnot(b20)); a[24] = b24.bitxor(b20.andnot(b21));
553            a[0] = a[0].bitxor(L::splat(rc));
554        }
555    }
556
557    #[test]
558    fn x4_lanes_matches_raw_and_ab() {
559        use std::arch::x86_64::*;
560        use std::time::Instant;
561        let mut seed = 0x1234_5678_9abc_def0u64;
562        let mut rnd = || { seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); seed };
563        let states: [[u64; 25]; 4] = std::array::from_fn(|_| std::array::from_fn(|_| rnd()));
564        let mut lanes: [L; 25] = std::array::from_fn(|i| L([states[0][i], states[1][i], states[2][i], states[3][i]]));
565        unsafe {
566            let mut raw: [__m256i; 25] = std::array::from_fn(|i| _mm256_set_epi64x(states[3][i] as i64, states[2][i] as i64, states[1][i] as i64, states[0][i] as i64));
567            keccak_f1600_x4(&mut raw);
568            keccak_f1600_x4_lanes(&mut lanes);
569            for i in 0..25 {
570                let mut got = [0u64; 4];
571                _mm256_storeu_si256(got.as_mut_ptr() as *mut __m256i, raw[i]);
572                assert_eq!(lanes[i].0, got, "Lanes4Word64 x4 Keccak must equal raw keccak_f1600_x4 (lane {i})");
573            }
574            // Also confirm lane 0 equals a single scalar keccak_f1600 of state 0.
575            let mut sc = states[0];
576            keccak_f1600(&mut sc);
577            let l0: [u64; 25] = std::array::from_fn(|i| lanes[i].0[0]);
578            assert_eq!(l0, sc, "lane 0 of the x4 must equal one scalar keccak_f1600");
579
580            // A/B: raw intrinsics vs lane-newtype, same run.
581            macro_rules! t {
582                ($l:expr, $init:expr, $body:expr) => {{
583                    for _ in 0..100 { let mut s = $init; std::hint::black_box($body(&mut s)); }
584                    let start = Instant::now();
585                    for _ in 0..20000u32 { let mut s = $init; std::hint::black_box($body(&mut s)); }
586                    eprintln!("{:<22} {:>8.1} ns/op", $l, start.elapsed().as_nanos() as f64 / 20000.0);
587                }};
588            }
589            let raw0: [__m256i; 25] = std::array::from_fn(|i| _mm256_set_epi64x(states[3][i] as i64, states[2][i] as i64, states[1][i] as i64, states[0][i] as i64));
590            let lanes0: [L; 25] = std::array::from_fn(|i| L([states[0][i], states[1][i], states[2][i], states[3][i]]));
591            eprintln!("\n=== 4-way Keccak-f[1600] A/B (this box) ===");
592            t!("raw keccak_f1600_x4", raw0, |s: &mut [__m256i; 25]| keccak_f1600_x4(s));
593            t!("lanes x4 (Logos form)", lanes0, |s: &mut [L; 25]| keccak_f1600_x4_lanes(s));
594        }
595    }
596}