Skip to main content

p3_binary_field/aes/
packed.rs

1//! Blocks of AES-field bytes, sixteen, thirty-two or sixty-four at a time.
2//!
3//! The block width is a parameter rather than three separate types.
4//!
5//! Every operation below is written once and specialized per width by the compiler.
6//!
7//! How many bytes one instruction covers is a separate question, settled by the target.
8//!
9//! A block wider than the register is swept in several passes.
10
11use core::iter::{Product, Sum};
12use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
13use core::{array, ptr};
14
15use p3_field::{
16    Algebra, Field, PackedField, PackedFieldPow2, PackedValue, PrimeCharacteristicRing,
17};
18use rand::distr::{Distribution, StandardUniform};
19use rand::{Rng, RngExt};
20
21use super::engine::{invert_slice, mul_slice};
22use super::{ByteMatrix, Rijndael8b};
23use crate::Gf2;
24use crate::gf2::characteristic_two_methods;
25
26/// The block width the scalar field advertises as its packing.
27///
28/// Sixty-four bytes is one half-kilobit register.
29///
30/// The widest instruction then runs once per block rather than four times.
31#[cfg(all(
32    target_arch = "x86_64",
33    target_feature = "gfni",
34    target_feature = "avx512f",
35    target_feature = "avx512bw"
36))]
37pub(super) const PACKING_WIDTH: usize = 64;
38
39/// The block width the scalar field advertises as its packing.
40///
41/// Sixteen bytes is the widest register the byte-wise instructions are guaranteed in.
42///
43/// It is also what a compiler vectorizes the portable fallback into.
44#[cfg(not(all(
45    target_arch = "x86_64",
46    target_feature = "gfni",
47    target_feature = "avx512f",
48    target_feature = "avx512bw"
49)))]
50pub(super) const PACKING_WIDTH: usize = 16;
51
52/// A block of elements of the AES field, held as plain consecutive bytes.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54// Needed to make the byte views below sound.
55#[repr(transparent)]
56#[must_use]
57pub struct PackedRijndael8b<const N: usize>([Rijndael8b; N]);
58
59impl<const N: usize> Default for PackedRijndael8b<N> {
60    #[inline]
61    fn default() -> Self {
62        Self::ZERO
63    }
64}
65
66impl<const N: usize> PackedRijndael8b<N> {
67    /// The same element in every position.
68    #[inline]
69    const fn splat(value: Rijndael8b) -> Self {
70        Self([value; N])
71    }
72
73    /// The block seen as plain bytes.
74    #[inline]
75    const fn bytes(&self) -> &[u8; N] {
76        // SAFETY: the scalar is `repr(transparent)` over a byte and this type over the array.
77        //
78        // The block is therefore exactly `N` consecutive bytes at the same alignment.
79        unsafe { &*ptr::from_ref(self).cast::<[u8; N]>() }
80    }
81
82    /// The block seen as plain bytes, for writing.
83    #[inline]
84    const fn bytes_mut(&mut self) -> &mut [u8; N] {
85        // SAFETY: as above, and every byte is a valid element, so no write can be out of range.
86        unsafe { &mut *ptr::from_mut(self).cast::<[u8; N]>() }
87    }
88
89    /// Every element replaced by its image under one `F_2`-linear map.
90    #[inline]
91    pub fn apply(mut self, map: ByteMatrix) -> Self {
92        map.apply_slice(self.bytes_mut());
93        self
94    }
95
96    /// Every element raised to the power `2^k`.
97    ///
98    /// One tabulated map covers any exponent.
99    ///
100    /// Repeated squaring would cost one product per step instead.
101    #[inline]
102    pub fn frobenius(self, power_log: usize) -> Self {
103        self.apply(Rijndael8b::frobenius_map(power_log))
104    }
105
106    /// Every element inverted, with zero sent to zero.
107    ///
108    /// The byte-wise hardware inverse is one instruction per register.
109    ///
110    /// The fallback is an addition chain of eleven products per element.
111    #[inline]
112    pub fn invert_or_zero(mut self) -> Self {
113        invert_slice(self.bytes_mut());
114        self
115    }
116}
117
118impl<const N: usize> From<Rijndael8b> for PackedRijndael8b<N> {
119    #[inline]
120    fn from(value: Rijndael8b) -> Self {
121        Self::splat(value)
122    }
123}
124
125impl<const N: usize> From<Gf2> for PackedRijndael8b<N> {
126    /// `GF(2)` is the prime subfield, embedded as `{ZERO, ONE}` in every position.
127    #[inline]
128    fn from(value: Gf2) -> Self {
129        Self::splat(Rijndael8b::from(value))
130    }
131}
132
133impl<const N: usize> Add for PackedRijndael8b<N> {
134    type Output = Self;
135
136    #[inline]
137    #[allow(clippy::suspicious_arithmetic_impl)]
138    fn add(self, rhs: Self) -> Self {
139        // Addition in characteristic 2 is `XOR`, position by position.
140        Self(array::from_fn(|i| self.0[i] + rhs.0[i]))
141    }
142}
143
144impl<const N: usize> Sub for PackedRijndael8b<N> {
145    type Output = Self;
146
147    #[inline]
148    #[allow(clippy::suspicious_arithmetic_impl)]
149    fn sub(self, rhs: Self) -> Self {
150        // Subtraction coincides with addition in characteristic 2.
151        self + rhs
152    }
153}
154
155impl<const N: usize> Neg for PackedRijndael8b<N> {
156    type Output = Self;
157
158    #[inline]
159    fn neg(self) -> Self {
160        // `-x = x` in characteristic 2.
161        self
162    }
163}
164
165impl<const N: usize> Mul for PackedRijndael8b<N> {
166    type Output = Self;
167
168    #[inline]
169    fn mul(self, rhs: Self) -> Self {
170        let mut out = self;
171        mul_slice(out.bytes_mut(), rhs.bytes());
172        out
173    }
174}
175
176impl<const N: usize> Div for PackedRijndael8b<N> {
177    type Output = Self;
178
179    /// # Panics
180    /// Panics if any position of the divisor is zero.
181    #[inline]
182    #[allow(clippy::suspicious_arithmetic_impl)]
183    fn div(self, rhs: Self) -> Self {
184        assert!(!rhs.bytes().contains(&0), "tried to invert zero in a block");
185        self * rhs.invert_or_zero()
186    }
187}
188
189impl<const N: usize> DivAssign for PackedRijndael8b<N> {
190    #[inline]
191    fn div_assign(&mut self, rhs: Self) {
192        *self = *self / rhs;
193    }
194}
195
196impl<const N: usize> Add<Rijndael8b> for PackedRijndael8b<N> {
197    type Output = Self;
198
199    #[inline]
200    fn add(self, rhs: Rijndael8b) -> Self {
201        self + Self::splat(rhs)
202    }
203}
204
205impl<const N: usize> Add<PackedRijndael8b<N>> for Rijndael8b {
206    type Output = PackedRijndael8b<N>;
207
208    #[inline]
209    fn add(self, rhs: PackedRijndael8b<N>) -> Self::Output {
210        rhs + self
211    }
212}
213
214impl<const N: usize> Sub<Rijndael8b> for PackedRijndael8b<N> {
215    type Output = Self;
216
217    #[inline]
218    fn sub(self, rhs: Rijndael8b) -> Self {
219        self - Self::splat(rhs)
220    }
221}
222
223impl<const N: usize> Sub<PackedRijndael8b<N>> for Rijndael8b {
224    type Output = PackedRijndael8b<N>;
225
226    #[inline]
227    fn sub(self, rhs: PackedRijndael8b<N>) -> Self::Output {
228        PackedRijndael8b::splat(self) - rhs
229    }
230}
231
232impl<const N: usize> Mul<Rijndael8b> for PackedRijndael8b<N> {
233    type Output = Self;
234
235    /// A broadcast plus the byte-wise product beats tabulating the factor as a map.
236    ///
237    /// Callers holding one factor across a loop should hoist its matrix and sweep with that.
238    #[inline]
239    fn mul(self, rhs: Rijndael8b) -> Self {
240        self * Self::splat(rhs)
241    }
242}
243
244impl<const N: usize> Mul<PackedRijndael8b<N>> for Rijndael8b {
245    type Output = PackedRijndael8b<N>;
246
247    #[inline]
248    fn mul(self, rhs: PackedRijndael8b<N>) -> Self::Output {
249        rhs * self
250    }
251}
252
253impl<const N: usize> Div<Rijndael8b> for PackedRijndael8b<N> {
254    type Output = Self;
255
256    #[inline]
257    #[allow(clippy::suspicious_arithmetic_impl)]
258    fn div(self, rhs: Rijndael8b) -> Self {
259        self * rhs.inverse()
260    }
261}
262
263impl<const N: usize> DivAssign<Rijndael8b> for PackedRijndael8b<N> {
264    #[inline]
265    #[allow(clippy::suspicious_op_assign_impl)]
266    fn div_assign(&mut self, rhs: Rijndael8b) {
267        *self *= rhs.inverse();
268    }
269}
270
271impl<const N: usize, T: Into<Self>> AddAssign<T> for PackedRijndael8b<N> {
272    #[inline]
273    fn add_assign(&mut self, rhs: T) {
274        *self = *self + rhs.into();
275    }
276}
277
278impl<const N: usize, T: Into<Self>> SubAssign<T> for PackedRijndael8b<N> {
279    #[inline]
280    fn sub_assign(&mut self, rhs: T) {
281        *self = *self - rhs.into();
282    }
283}
284
285impl<const N: usize, T: Into<Self>> MulAssign<T> for PackedRijndael8b<N> {
286    #[inline]
287    fn mul_assign(&mut self, rhs: T) {
288        *self = *self * rhs.into();
289    }
290}
291
292impl<const N: usize> Sum for PackedRijndael8b<N> {
293    #[inline]
294    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
295        iter.reduce(|x, y| x + y).unwrap_or(Self::ZERO)
296    }
297}
298
299impl<const N: usize> Product for PackedRijndael8b<N> {
300    #[inline]
301    fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
302        iter.reduce(|x, y| x * y).unwrap_or(Self::ONE)
303    }
304}
305
306impl<const N: usize> Sum<Rijndael8b> for PackedRijndael8b<N> {
307    #[inline]
308    fn sum<I: Iterator<Item = Rijndael8b>>(iter: I) -> Self {
309        iter.sum::<Rijndael8b>().into()
310    }
311}
312
313impl<const N: usize> Product<Rijndael8b> for PackedRijndael8b<N> {
314    #[inline]
315    fn product<I: Iterator<Item = Rijndael8b>>(iter: I) -> Self {
316        iter.product::<Rijndael8b>().into()
317    }
318}
319
320impl<const N: usize> Distribution<PackedRijndael8b<N>> for StandardUniform {
321    #[inline]
322    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PackedRijndael8b<N> {
323        PackedRijndael8b(array::from_fn(|_| rng.random()))
324    }
325}
326
327impl<const N: usize> PrimeCharacteristicRing for PackedRijndael8b<N> {
328    type PrimeSubfield = Gf2;
329
330    const ZERO: Self = Self::splat(Rijndael8b::ZERO);
331    const ONE: Self = Self::splat(Rijndael8b::ONE);
332    // The characteristic is 2, so `TWO = ONE + ONE = ZERO`.
333    const TWO: Self = Self::splat(Rijndael8b::ZERO);
334    // The characteristic is 2, so `NEG_ONE = ONE`.
335    const NEG_ONE: Self = Self::splat(Rijndael8b::ONE);
336
337    #[inline]
338    fn from_prime_subfield(f: Self::PrimeSubfield) -> Self {
339        Self::splat(Rijndael8b::from_prime_subfield(f))
340    }
341
342    characteristic_two_methods!();
343
344    /// Raising to `2^k` is one tabulated map, whatever the exponent.
345    #[inline]
346    fn exp_power_of_2(&self, power_log: usize) -> Self {
347        self.frobenius(power_log)
348    }
349}
350
351impl<const N: usize> Algebra<Rijndael8b> for PackedRijndael8b<N> {}
352
353impl<const N: usize> Algebra<Gf2> for PackedRijndael8b<N> {}
354
355impl<const N: usize> Add<Gf2> for PackedRijndael8b<N> {
356    type Output = Self;
357
358    #[inline]
359    fn add(self, rhs: Gf2) -> Self {
360        self + Self::from(rhs)
361    }
362}
363
364impl<const N: usize> Sub<Gf2> for PackedRijndael8b<N> {
365    type Output = Self;
366
367    #[inline]
368    fn sub(self, rhs: Gf2) -> Self {
369        self - Self::from(rhs)
370    }
371}
372
373impl<const N: usize> Mul<Gf2> for PackedRijndael8b<N> {
374    type Output = Self;
375
376    #[inline]
377    fn mul(self, rhs: Gf2) -> Self {
378        // The prime subfield has two elements, so this keeps the block or clears it.
379        if rhs.is_one() { self } else { Self::ZERO }
380    }
381}
382
383// SAFETY: the type is `repr(transparent)` over the array, so the reference casts below hold.
384unsafe impl<const N: usize> PackedValue for PackedRijndael8b<N> {
385    type Value = Rijndael8b;
386
387    // The packed contract promises a power of two, and the interleave below indexes on it.
388    //
389    // Asserting here rejects any other width at the first use of the trait.
390    const WIDTH: usize = {
391        assert!(
392            N.is_power_of_two(),
393            "the block width must be a power of two"
394        );
395        N
396    };
397
398    #[inline]
399    fn from_slice(slice: &[Self::Value]) -> &Self {
400        assert_eq!(slice.len(), Self::WIDTH);
401        // SAFETY: the length check above makes the source exactly one block.
402        unsafe { &*slice.as_ptr().cast() }
403    }
404
405    #[inline]
406    fn from_slice_mut(slice: &mut [Self::Value]) -> &mut Self {
407        assert_eq!(slice.len(), Self::WIDTH);
408        // SAFETY: the length check above makes the source exactly one block.
409        unsafe { &mut *slice.as_mut_ptr().cast() }
410    }
411
412    #[inline]
413    fn as_slice(&self) -> &[Self::Value] {
414        &self.0
415    }
416
417    #[inline]
418    fn as_slice_mut(&mut self) -> &mut [Self::Value] {
419        &mut self.0
420    }
421
422    #[inline]
423    fn from_fn<F: FnMut(usize) -> Self::Value>(f: F) -> Self {
424        Self(array::from_fn(f))
425    }
426}
427
428// SAFETY: the transparent array satisfies the packed layout contract.
429//
430// Every operation acts on each byte independently.
431unsafe impl<const N: usize> PackedField for PackedRijndael8b<N> {
432    type Scalar = Rijndael8b;
433}
434
435// SAFETY: the widths this type is used at are powers of two.
436unsafe impl<const N: usize> PackedFieldPow2 for PackedRijndael8b<N> {
437    /// # Panics
438    /// Panics if the block length does not divide the width, or is not a power of two.
439    #[inline]
440    fn interleave(&self, other: Self, block_len: usize) -> (Self, Self) {
441        const {
442            assert!(
443                N.is_power_of_two(),
444                "the block width must be a power of two"
445            );
446        };
447        assert!(
448            block_len.is_power_of_two() && block_len <= N && N.is_multiple_of(block_len),
449            "unsupported block_len"
450        );
451
452        // One block each leaves nothing to pair up, so the two vectors pass through.
453        if block_len == N {
454            return (*self, other);
455        }
456
457        // Stack the two vectors, cut them into two-by-two matrices of blocks, transpose those.
458        //
459        //     left  blocks: a_0, b_0, a_2, b_2, ...
460        //     right blocks: a_1, b_1, a_3, b_3, ...
461        let block = |from: &Self, index: usize, offset: usize| from.0[index * block_len + offset];
462        let left = array::from_fn(|i| {
463            let (m, offset) = (i / block_len, i % block_len);
464            if m % 2 == 0 {
465                block(self, m, offset)
466            } else {
467                block(&other, m - 1, offset)
468            }
469        });
470        let right = array::from_fn(|i| {
471            let (m, offset) = (i / block_len, i % block_len);
472            if m % 2 == 0 {
473                block(self, m + 1, offset)
474            } else {
475                block(&other, m, offset)
476            }
477        });
478        (Self(left), Self(right))
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use alloc::vec::Vec;
485
486    use p3_field::{PackedFieldPow2, PackedValue, PrimeCharacteristicRing};
487    use proptest::prelude::*;
488
489    use super::PackedRijndael8b;
490    use crate::Rijndael8b;
491    use crate::aes::{invert_byte, mul_bytes};
492
493    /// The block widths the crate exports, so every sweep length is covered.
494    ///
495    /// Sixteen is one narrow register, sixty-four is one wide one, and thirty-two is neither.
496    type Narrow = PackedRijndael8b<16>;
497    type Middle = PackedRijndael8b<32>;
498    type Wide = PackedRijndael8b<64>;
499
500    /// The bytes a random search is unlikely to reach.
501    ///
502    /// ```text
503    ///     0     the absorbing element
504    ///     1     the identity
505    ///     0x1b  the modulus tail, so one more fold happens
506    ///     0x80  the highest basis vector, so doubling it overflows
507    /// ```
508    const SPECIAL: [u8; 4] = [0, 1, 0x1b, 0x80];
509
510    /// One extreme byte per position, cycling through the corners.
511    fn specials<const N: usize>() -> PackedRijndael8b<N> {
512        PackedValue::from_fn(|i| Rijndael8b::from_byte(SPECIAL[i % SPECIAL.len()]))
513    }
514
515    /// The block operations against the scalar routines, one position at a time.
516    fn block_agrees<const N: usize>(a: [u8; N], b: [u8; N]) -> Result<(), TestCaseError> {
517        let pack =
518            |bytes: [u8; N]| PackedRijndael8b::<N>::from_fn(|i| Rijndael8b::from_byte(bytes[i]));
519        let (x, y) = (pack(a), pack(b));
520
521        let expected = pack(core::array::from_fn(|i| mul_bytes(a[i], b[i])));
522        prop_assert_eq!(x * y, expected);
523
524        let expected = pack(core::array::from_fn(|i| a[i] ^ b[i]));
525        prop_assert_eq!(x + y, expected);
526
527        // Scaling by the first position's byte, which rides the map engine rather than a product.
528        let scalar = Rijndael8b::from_byte(b[0]);
529        let expected = pack(core::array::from_fn(|i| mul_bytes(a[i], b[0])));
530        prop_assert_eq!(x * scalar, expected);
531
532        prop_assert_eq!(
533            x.invert_or_zero(),
534            pack(core::array::from_fn(|i| invert_byte(a[i])))
535        );
536
537        Ok(())
538    }
539
540    #[test]
541    fn every_width_agrees_with_the_scalar_routines_at_the_corners() {
542        // Fixture state: each width, with every corner pair placed at position 0.
543        //
544        // The other positions rotate through the corners.
545        //
546        // A value that leaks across a register boundary then lands on a different extreme.
547        for (i, &x) in SPECIAL.iter().enumerate() {
548            for (j, &y) in SPECIAL.iter().enumerate() {
549                let a = |lane: usize| {
550                    if lane == 0 {
551                        x
552                    } else {
553                        SPECIAL[(i + lane) % 4]
554                    }
555                };
556                let b = |lane: usize| {
557                    if lane == 0 {
558                        y
559                    } else {
560                        SPECIAL[(j + lane) % 4]
561                    }
562                };
563                block_agrees::<16>(core::array::from_fn(a), core::array::from_fn(b)).unwrap();
564                block_agrees::<32>(core::array::from_fn(a), core::array::from_fn(b)).unwrap();
565                block_agrees::<64>(core::array::from_fn(a), core::array::from_fn(b)).unwrap();
566            }
567        }
568    }
569
570    proptest! {
571        #![proptest_config(ProptestConfig::with_cases(256))]
572
573        #[test]
574        fn every_width_agrees_with_the_scalar_routines(
575            a in prop::collection::vec(any::<u8>(), 64),
576            b in prop::collection::vec(any::<u8>(), 64),
577        ) {
578            let take = |from: &Vec<u8>, n: usize| -> Vec<u8> { from[..n].to_vec() };
579            let fixed = |v: Vec<u8>| -> [u8; 16] { core::array::from_fn(|i| v[i]) };
580            block_agrees::<16>(fixed(take(&a, 16)), fixed(take(&b, 16)))?;
581
582            let fixed = |v: Vec<u8>| -> [u8; 32] { core::array::from_fn(|i| v[i]) };
583            block_agrees::<32>(fixed(take(&a, 32)), fixed(take(&b, 32)))?;
584
585            let fixed = |v: Vec<u8>| -> [u8; 64] { core::array::from_fn(|i| v[i]) };
586            block_agrees::<64>(fixed(a), fixed(b))?;
587        }
588    }
589
590    #[test]
591    fn the_interleave_convention_is_the_two_by_two_transpose() {
592        // Invariant: stack the two vectors, cut into two-by-two matrices of blocks, transpose.
593        //
594        // Fixture state: sixteen positions carrying 0..15 against 16..31.
595        //
596        //     block length 1:  left takes a_0 b_0 a_2 b_2 ...
597        //     naive reading:   left would take a_0 b_0 a_1 b_1 ...
598        //
599        // The two disagree from the third position on, which is what pins the convention.
600        let pack = |base: u8| Narrow::from_fn(|i| Rijndael8b::from_byte(base + i as u8));
601        let (a, b) = (pack(0), pack(16));
602        let expect = |bytes: [u8; 16]| Narrow::from_fn(|i| Rijndael8b::from_byte(bytes[i]));
603
604        let (left, right) = a.interleave(b, 1);
605        let want_left = [0, 16, 2, 18, 4, 20, 6, 22, 8, 24, 10, 26, 12, 28, 14, 30];
606        assert_eq!(left, expect(want_left));
607        assert_eq!(
608            right,
609            expect([1, 17, 3, 19, 5, 21, 7, 23, 9, 25, 11, 27, 13, 29, 15, 31])
610        );
611
612        // The naive reading would put a_1 where the transpose puts a_2.
613        let naive_left = [0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23];
614        assert_ne!(want_left, naive_left, "the anchor must discriminate");
615
616        let (left, right) = a.interleave(b, 2);
617        assert_eq!(
618            left,
619            expect([0, 1, 16, 17, 4, 5, 20, 21, 8, 9, 24, 25, 12, 13, 28, 29])
620        );
621        assert_eq!(
622            right,
623            expect([2, 3, 18, 19, 6, 7, 22, 23, 10, 11, 26, 27, 14, 15, 30, 31])
624        );
625
626        let (left, right) = a.interleave(b, 8);
627        assert_eq!(
628            left,
629            expect([0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23])
630        );
631        assert_eq!(
632            right,
633            expect([8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31])
634        );
635
636        // One block each leaves nothing to pair up.
637        assert_eq!(a.interleave(b, 16), (a, b));
638    }
639
640    #[test]
641    #[should_panic = "tried to invert zero in a block"]
642    fn dividing_by_a_block_holding_zero_is_rejected() {
643        let _quotient = Narrow::ONE / Narrow::ZERO;
644    }
645
646    // Each suite lives in its own module, so the three widths keep separate test names.
647    //
648    // The suites expand into a module of their own, which is why the paths reach out twice.
649    mod narrow {
650        use p3_field_testing::test_packed_binary_field;
651
652        test_packed_binary_field!(
653            super::super::Narrow,
654            &[super::super::Narrow::ZERO],
655            &[super::super::Narrow::ONE],
656            super::super::specials::<16>()
657        );
658    }
659
660    mod middle {
661        use p3_field_testing::test_packed_binary_field;
662
663        test_packed_binary_field!(
664            super::super::Middle,
665            &[super::super::Middle::ZERO],
666            &[super::super::Middle::ONE],
667            super::super::specials::<32>()
668        );
669    }
670
671    mod wide {
672        use p3_field_testing::test_packed_binary_field;
673
674        test_packed_binary_field!(
675            super::super::Wide,
676            &[super::super::Wide::ZERO],
677            &[super::super::Wide::ONE],
678            super::super::specials::<64>()
679        );
680    }
681}