Skip to main content

p3_symmetric/
compression.rs

1use crate::hasher::CryptographicHasher;
2use crate::permutation::CryptographicPermutation;
3
4/// An `N`-to-1 compression function collision-resistant in a hash tree setting.
5///
6/// Unlike `CompressionFunction`, it may not be collision-resistant in general.
7/// Instead it is only collision-resistant in hash-tree like settings where
8/// the preimage of a non-leaf node must consist of compression outputs.
9pub trait PseudoCompressionFunction<T, const N: usize>: Clone {
10    /// Number of independent compressions this implementation performs most efficiently in one call.
11    ///
12    /// One means every group is compressed on its own, which is the behaviour of a plain scalar permutation.
13    /// A vectorized implementation reports how many independent states its permutation advances at once.
14    ///
15    /// Callers read this only to decide whether grouping compressions is worth the bookkeeping.
16    const LANES: usize = 1;
17
18    fn compress(&self, input: [T; N]) -> T;
19
20    /// Compress a batch of input groups, one output per group.
21    ///
22    /// ```text
23    ///     inputs: [ grp_0 | grp_1 | ... | grp_{m-1} ]   m groups of N inputs
24    ///     out:    [ dig_0 | dig_1 | ... | dig_{m-1} ]   m outputs
25    /// ```
26    ///
27    /// The default compresses the groups one at a time.
28    /// An override exists purely to exploit vector hardware and must return the very same outputs.
29    ///
30    /// # Panics
31    ///
32    /// Panics if the batch is ragged: the group count must equal the output count.
33    fn compress_many(&self, inputs: &[[T; N]], out: &mut [T])
34    where
35        T: Clone,
36    {
37        // A ragged batch is a caller bug, so it fails loudly rather than dropping groups.
38        assert_eq!(
39            inputs.len(),
40            out.len(),
41            "group count ({}) must equal the output count ({})",
42            inputs.len(),
43            out.len()
44        );
45
46        // Walk the groups in order so the outputs land in the caller's order.
47        for (output, group) in out.iter_mut().zip(inputs) {
48            *output = self.compress(group.clone());
49        }
50    }
51}
52
53/// An `N`-to-1 compression function.
54pub trait CompressionFunction<T, const N: usize>: PseudoCompressionFunction<T, N> {}
55
56#[derive(Clone, Debug)]
57pub struct TruncatedPermutation<InnerP, const N: usize, const CHUNK: usize, const WIDTH: usize> {
58    inner_permutation: InnerP,
59}
60
61impl<InnerP, const N: usize, const CHUNK: usize, const WIDTH: usize>
62    TruncatedPermutation<InnerP, N, CHUNK, WIDTH>
63{
64    pub const fn new(inner_permutation: InnerP) -> Self {
65        const {
66            assert!(N > 0);
67            assert!(CHUNK > 0);
68            assert!(CHUNK * N <= WIDTH);
69        }
70        Self { inner_permutation }
71    }
72}
73
74impl<T, InnerP, const N: usize, const CHUNK: usize, const WIDTH: usize>
75    PseudoCompressionFunction<[T; CHUNK], N> for TruncatedPermutation<InnerP, N, CHUNK, WIDTH>
76where
77    T: Copy + Default,
78    InnerP: CryptographicPermutation<[T; WIDTH]>,
79{
80    fn compress(&self, input: [[T; CHUNK]; N]) -> [T; CHUNK] {
81        let mut pre = [T::default(); WIDTH];
82        for i in 0..N {
83            pre[i * CHUNK..(i + 1) * CHUNK].copy_from_slice(&input[i]);
84        }
85        let post = self.inner_permutation.permute(pre);
86        post[..CHUNK].try_into().unwrap()
87    }
88}
89
90#[derive(Clone, Debug)]
91pub struct CompressionFunctionFromHasher<H, const N: usize, const CHUNK: usize> {
92    hasher: H,
93}
94
95impl<H, const N: usize, const CHUNK: usize> CompressionFunctionFromHasher<H, N, CHUNK> {
96    pub const fn new(hasher: H) -> Self {
97        Self { hasher }
98    }
99}
100
101impl<T, H, const N: usize, const CHUNK: usize> PseudoCompressionFunction<[T; CHUNK], N>
102    for CompressionFunctionFromHasher<H, N, CHUNK>
103where
104    T: Clone,
105    H: CryptographicHasher<T, [T; CHUNK]>,
106{
107    const LANES: usize = <H as CryptographicHasher<T, [T; CHUNK]>>::LANES;
108
109    fn compress(&self, input: [[T; CHUNK]; N]) -> [T; CHUNK] {
110        self.hasher.hash_iter(input.into_iter().flatten())
111    }
112
113    fn compress_many(&self, inputs: &[[[T; CHUNK]; N]], out: &mut [[T; CHUNK]]) {
114        // A ragged batch is a caller bug, so it fails loudly rather than dropping groups.
115        assert_eq!(
116            inputs.len(),
117            out.len(),
118            "group count ({}) must equal the output count ({})",
119            inputs.len(),
120            out.len()
121        );
122
123        // A group is `N` adjacent chunks of `CHUNK` items.
124        // A run of groups is therefore already one flat run of items:
125        //
126        //     inputs: [[c0 c1] [c2 c3] ...]  ->  flat: [c0 c1 c2 c3 ...]
127        //
128        // Flattening twice hands the hasher the concatenated preimages directly.
129        // No copying is needed.
130        let messages = inputs.as_flattened().as_flattened();
131
132        // Each message is `N * CHUNK` items long, so the batch hasher can split them itself.
133        // Equal counts make that split exact.
134        self.hasher.hash_many(messages, out);
135    }
136}
137
138impl<T, H, const N: usize, const CHUNK: usize> CompressionFunction<[T; CHUNK], N>
139    for CompressionFunctionFromHasher<H, N, CHUNK>
140where
141    T: Clone,
142    H: CryptographicHasher<T, [T; CHUNK]>,
143{
144}
145
146#[cfg(test)]
147mod tests {
148    use alloc::vec;
149    use alloc::vec::Vec;
150    use core::array;
151
152    use super::*;
153    use crate::Permutation;
154
155    #[derive(Clone)]
156    struct MockPermutation;
157
158    impl<T, const WIDTH: usize> Permutation<[T; WIDTH]> for MockPermutation
159    where
160        T: Copy + core::ops::Add<Output = T> + Default,
161    {
162        fn permute_mut(&self, input: &mut [T; WIDTH]) {
163            let sum: T = input.iter().copied().fold(T::default(), |acc, x| acc + x);
164            // Simplest impl: set every element to the sum
165            *input = [sum; WIDTH];
166        }
167    }
168
169    impl<T, const WIDTH: usize> CryptographicPermutation<[T; WIDTH]> for MockPermutation where
170        T: Copy + core::ops::Add<Output = T> + Default
171    {
172    }
173
174    #[derive(Clone)]
175    struct MockHasher;
176
177    impl<const CHUNK: usize> CryptographicHasher<u64, [u64; CHUNK]> for MockHasher {
178        fn hash_iter<I: IntoIterator<Item = u64>>(&self, iter: I) -> [u64; CHUNK] {
179            let sum: u64 = iter.into_iter().sum();
180            // Simplest impl: set every element to the sum
181            [sum; CHUNK]
182        }
183    }
184
185    #[test]
186    fn test_truncated_permutation_compress() {
187        const N: usize = 2;
188        const CHUNK: usize = 4;
189        const WIDTH: usize = 8;
190
191        let permutation = MockPermutation;
192        let compressor = TruncatedPermutation::<MockPermutation, N, CHUNK, WIDTH>::new(permutation);
193
194        let input: [[u64; CHUNK]; N] = [[1, 2, 3, 4], [5, 6, 7, 8]];
195        let output = compressor.compress(input);
196        let expected_sum = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8;
197
198        assert_eq!(output, [expected_sum; CHUNK]);
199    }
200
201    #[test]
202    fn test_compression_function_from_hasher_compress() {
203        const N: usize = 2;
204        const CHUNK: usize = 4;
205
206        let hasher = MockHasher;
207        let compressor = CompressionFunctionFromHasher::<MockHasher, N, CHUNK>::new(hasher);
208
209        let input = [[10, 20, 30, 40], [50, 60, 70, 80]];
210        let output = compressor.compress(input);
211        let expected_sum = 10 + 20 + 30 + 40 + 50 + 60 + 70 + 80;
212
213        assert_eq!(output, [expected_sum; CHUNK]);
214    }
215
216    #[test]
217    fn test_truncated_permutation_with_zeros() {
218        const N: usize = 2;
219        const CHUNK: usize = 4;
220        const WIDTH: usize = 8;
221
222        let permutation = MockPermutation;
223        let compressor = TruncatedPermutation::<MockPermutation, N, CHUNK, WIDTH>::new(permutation);
224
225        let input: [[u64; CHUNK]; N] = [[0, 0, 0, 0], [0, 0, 0, 0]];
226        let output = compressor.compress(input);
227
228        assert_eq!(output, [0; CHUNK]);
229    }
230
231    #[test]
232    fn test_truncated_permutation_with_extra_width() {
233        const N: usize = 2;
234        const CHUNK: usize = 3;
235        const WIDTH: usize = 10; // More than `CHUNK * N` (6 < 10)
236
237        let permutation = MockPermutation;
238        let compressor = TruncatedPermutation::<MockPermutation, N, CHUNK, WIDTH>::new(permutation);
239
240        let input: [[u64; CHUNK]; N] = [[1, 2, 3], [4, 5, 6]];
241        let output = compressor.compress(input);
242
243        let expected_sum = 1 + 2 + 3 + 4 + 5 + 6;
244
245        assert_eq!(
246            output, [expected_sum; CHUNK],
247            "Compression should correctly handle extra WIDTH space."
248        );
249    }
250
251    #[test]
252    fn compress_many_matches_compressing_each_group_alone() {
253        const N: usize = 2;
254        const CHUNK: usize = 4;
255        const WIDTH: usize = 8;
256
257        let compressor = TruncatedPermutation::<_, N, CHUNK, WIDTH>::new(MockPermutation);
258
259        // Three groups of two four-word children.
260        let groups: [[[u64; CHUNK]; N]; 3] =
261            array::from_fn(|g| array::from_fn(|n| array::from_fn(|i| (g * 8 + n * 4 + i) as u64)));
262
263        let mut batched = [[0u64; CHUNK]; 3];
264        compressor.compress_many(&groups, &mut batched);
265
266        let expected: [[u64; CHUNK]; 3] = groups.map(|group| compressor.compress(group));
267        assert_eq!(batched, expected);
268    }
269
270    #[test]
271    fn compress_many_of_an_empty_batch_writes_nothing() {
272        const N: usize = 2;
273        const CHUNK: usize = 4;
274        const WIDTH: usize = 8;
275
276        let compressor = TruncatedPermutation::<_, N, CHUNK, WIDTH>::new(MockPermutation);
277
278        // Both counts are zero, which is a well-formed batch of no groups.
279        let groups: [[[u64; CHUNK]; N]; 0] = [];
280        compressor.compress_many(&groups, &mut []);
281    }
282
283    #[test]
284    #[should_panic(expected = "must equal the output count")]
285    fn compress_many_rejects_a_ragged_batch() {
286        const N: usize = 2;
287        const CHUNK: usize = 4;
288        const WIDTH: usize = 8;
289
290        let compressor = TruncatedPermutation::<_, N, CHUNK, WIDTH>::new(MockPermutation);
291
292        // Two groups with one slot for their outputs: the caller has miscounted, so the batch
293        // fails rather than quietly compressing only the first group.
294        let groups = [[[0u64; CHUNK]; N]; 2];
295        let mut out = [[0u64; CHUNK]; 1];
296        compressor.compress_many(&groups, &mut out);
297    }
298
299    #[test]
300    fn compress_many_from_hasher_matches_compressing_each_group_alone() {
301        const N: usize = 2;
302        const CHUNK: usize = 4;
303
304        let compressor = CompressionFunctionFromHasher::<MockHasher, N, CHUNK>::new(MockHasher);
305
306        let groups: [[[u64; CHUNK]; N]; 3] =
307            array::from_fn(|g| array::from_fn(|n| array::from_fn(|i| (g * 8 + n * 4 + i) as u64)));
308
309        let mut batched = vec![[0u64; CHUNK]; 3];
310        compressor.compress_many(&groups, &mut batched);
311
312        let expected: Vec<[u64; CHUNK]> = groups.iter().map(|&g| compressor.compress(g)).collect();
313        assert_eq!(batched, expected);
314    }
315
316    #[test]
317    #[should_panic(expected = "must equal the output count")]
318    fn compress_many_from_hasher_rejects_a_ragged_batch() {
319        const N: usize = 2;
320        const CHUNK: usize = 4;
321
322        let compressor = CompressionFunctionFromHasher::<MockHasher, N, CHUNK>::new(MockHasher);
323
324        // The adapter hands the whole flattened run to the batch hasher, so unequal counts would
325        // silently redraw every message boundary.
326        let groups = [[[0u64; CHUNK]; N]; 3];
327        let mut out = [[0u64; CHUNK]; 2];
328        compressor.compress_many(&groups, &mut out);
329    }
330}