p3_symmetric/hasher.rs
1/// A generic trait for cryptographic hashers that consume an arbitrary sequence of input items
2/// and produce a fixed-size output.
3///
4/// This trait abstracts over hash functions in a flexible way, supporting both field elements,
5/// scalars, or any other data type that implements `Clone`.
6pub trait CryptographicHasher<Item: Clone, Out>: Clone {
7 /// Number of equal-length messages this implementation hashes most efficiently in one call.
8 ///
9 /// One means every message is hashed on its own, which is the behaviour of a plain scalar sponge.
10 /// A vectorized implementation reports how many independent states its permutation advances at once.
11 ///
12 /// Callers read this only to decide whether grouping messages is worth the bookkeeping.
13 const LANES: usize = 1;
14
15 /// Whether materializing a compound input iterator can improve hashing throughput.
16 ///
17 /// Callers concatenating disjoint inputs may use this hint to reuse a contiguous
18 /// staging buffer and pass its simple iterator to [`Self::hash_iter`]. It is not
19 /// a request to copy inputs that already have an efficient iterator, nor does it
20 /// affect the digest: the input sequence and message boundaries must stay identical.
21 const PREFER_CONTIGUOUS_INPUT: bool = false;
22
23 /// Hash an iterator of input items.
24 /// # Arguments
25 /// - `input`: An iterator over items to be hashed.
26 ///
27 /// # Returns
28 /// A fixed-size digest of type `Out`.
29 fn hash_iter<I>(&self, input: I) -> Out
30 where
31 I: IntoIterator<Item = Item>;
32
33 /// Hash an iterator of slices, by flattening it into a single stream of items.
34 ///
35 /// # Arguments
36 /// - `input`: An iterator over slices of items to hash.
37 ///
38 /// # Returns
39 /// A fixed-size digest of type `Out`.
40 fn hash_iter_slices<'a, I>(&self, input: I) -> Out
41 where
42 I: IntoIterator<Item = &'a [Item]>,
43 Item: 'a,
44 {
45 self.hash_iter(input.into_iter().flatten().cloned())
46 }
47
48 /// Hash a single slice of items.
49 ///
50 /// # Arguments
51 /// - `input`: A slice of items to hash.
52 ///
53 /// # Returns
54 /// A fixed-size digest of type `Out`.
55 fn hash_slice(&self, input: &[Item]) -> Out {
56 self.hash_iter_slices(core::iter::once(input))
57 }
58
59 /// Hash a single item.
60 ///
61 /// # Arguments
62 /// - `input`: A single item to hash.
63 ///
64 /// # Returns
65 /// A fixed-size digest of type `Out`.
66 fn hash_item(&self, input: Item) -> Out {
67 self.hash_slice(&[input])
68 }
69
70 /// Hash a batch of equal-length messages, one digest per message.
71 ///
72 /// All messages sit back to back in a single slice.
73 /// The common message length is the input length divided by the digest count:
74 ///
75 /// ```text
76 /// input: [ msg_0 | msg_1 | ... | msg_{m-1} ] m * len items
77 /// out: [ dig_0 | dig_1 | ... | dig_{m-1} ] m digests
78 /// ```
79 ///
80 /// The default hashes the messages one at a time.
81 /// An override exists purely to exploit vector hardware and must return the very same digests.
82 ///
83 /// # Panics
84 ///
85 /// Panics if the batch is ragged: the input length must be a whole multiple of the digest count.
86 fn hash_many(&self, input: &[Item], out: &mut [Out]) {
87 // No digests requested means there is nothing to read from the input.
88 if out.is_empty() {
89 return;
90 }
91
92 // Every message has the same length, so the split is exact by contract.
93 assert!(
94 input.len().is_multiple_of(out.len()),
95 "input length ({}) must be a whole multiple of the digest count ({})",
96 input.len(),
97 out.len()
98 );
99 let len = input.len() / out.len();
100
101 // Zero-length messages all hash to the same digest.
102 // They are also the one case a chunked walk over the input cannot express.
103 if len == 0 {
104 for digest in out.iter_mut() {
105 *digest = self.hash_slice(&[]);
106 }
107 return;
108 }
109
110 // Walk the messages in order so the digests land in the caller's order.
111 for (digest, message) in out.iter_mut().zip(input.chunks_exact(len)) {
112 *digest = self.hash_slice(message);
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use alloc::vec;
120 use core::array;
121
122 use crate::CryptographicHasher;
123
124 /// A byte hasher whose digest carries both a running fold and the message length.
125 ///
126 /// The multiplier makes the digest order sensitive.
127 /// The counter makes a message of a different length impossible to collide with.
128 #[derive(Clone)]
129 struct Fold;
130
131 impl CryptographicHasher<u8, [u8; 2]> for Fold {
132 fn hash_iter<I>(&self, input: I) -> [u8; 2]
133 where
134 I: IntoIterator<Item = u8>,
135 {
136 let mut acc = 0u8;
137 let mut count = 0u8;
138 for byte in input {
139 acc = acc.wrapping_mul(31).wrapping_add(byte);
140 count = count.wrapping_add(1);
141 }
142 [acc, count]
143 }
144 }
145
146 #[test]
147 fn hash_many_matches_hashing_each_message_alone() {
148 // Four messages of three bytes, laid out back to back.
149 let input: [u8; 12] = array::from_fn(|i| i as u8);
150
151 let mut batched = [[0u8; 2]; 4];
152 Fold.hash_many(&input, &mut batched);
153
154 let expected: [[u8; 2]; 4] = array::from_fn(|i| Fold.hash_slice(&input[3 * i..][..3]));
155 assert_eq!(batched, expected);
156 }
157
158 #[test]
159 fn hash_many_of_zero_length_messages_hashes_the_empty_message() {
160 // No input with digests requested means every message is empty.
161 let mut out = vec![[1u8; 2]; 3];
162 Fold.hash_many(&[], &mut out);
163
164 assert_eq!(out, vec![Fold.hash_slice(&[]); 3]);
165 }
166
167 #[test]
168 fn hash_many_reads_nothing_when_no_digests_are_requested() {
169 // A message length cannot be derived from zero digests, so the input is left untouched
170 // instead of tripping the multiple check on a length that divides nothing.
171 Fold.hash_many(&[1, 2, 3], &mut []);
172 }
173
174 #[test]
175 #[should_panic(expected = "must be a whole multiple")]
176 fn hash_many_rejects_ragged_input() {
177 // Five bytes cannot split into two equal messages.
178 let mut out = [[0u8; 2]; 2];
179 Fold.hash_many(&[1, 2, 3, 4, 5], &mut out);
180 }
181}