p3_symmetric/sponge.rs
1//! Sponge-based hash functions built from cryptographic permutations.
2//!
3//! # Background
4//!
5//! A sponge \[BDPV07\] hashes an input using a fixed-width permutation P.
6//! The b-element state has two regions:
7//!
8//! ```text
9//! +--------------------------------------------+
10//! | state[0 .. r] | state[r .. b] |
11//! | rate (outer) | capacity (inner) |
12//! +--------------------------------------------+
13//! ```
14//!
15//! - **Rate (r)** -- absorbs input, produces output.
16//! - **Capacity (c = b - r)** -- never exposed directly.
17//! Provides collision resistance up to |F|^{c/2} queries \[BDPA08\].
18//!
19//! This module uses the **overwrite** variant: each input block
20//! overwrites (rather than XORs into) the rate portion.
21//! Security carries over from the standard sponge \[BDPA08, AMP10\].
22//!
23//! # Variants
24//!
25//! This module provides two sponge variants for different use cases:
26//!
27//! - `PaddingFreeSponge` -- for **fixed-length** inputs where the
28//! number of elements to hash is predetermined by the protocol and
29//! not controlled by the attacker. Collision-resistant in this
30//! setting. Not suitable when the attacker controls input length.
31//!
32//! - `Pad10Sponge` -- for **variable-length** inputs where the number
33//! of elements can be chosen at runtime. Also secure for fixed-
34//! length inputs but slightly slower than the padding-free variant.
35//!
36//! # Why Padding Matters
37//!
38//! Without padding, different-length messages can collide trivially.
39//!
40//! ```text
41//! WIDTH = 8, RATE = 4, capacity = 4
42//!
43//! Message A (length 10):
44//!
45//! block 1 block 2 partial
46//! +--------------+ +--------------+ +--------+
47//! | h0 h1 h2 h3 | | h4 h5 h6 h7 | | h8 h9 |
48//! +--------------+ +--------------+ +--------+
49//!
50//! Step 1 – absorb block 1:
51//! state = [h0, h1, h2, h3 | 0, 0, 0, 0] -> P
52//! state = [p0, p1, p2, p3 | p4, p5, p6, p7]
53//!
54//! Step 2 – absorb block 2:
55//! state = [h4, h5, h6, h7 | p4, p5, p6, p7] -> P
56//! state = [q0, q1, q2, q3 | q4, q5, q6, q7]
57//!
58//! Step 3 – absorb partial (only 2 elements):
59//! overwrite positions 0..2, leave 2..4 untouched:
60//! state = [h8, h9, q2, q3 | q4, q5, q6, q7] -> P -> digest
61//! ^^ ^^
62//! still hold old values from q
63//! ```
64//!
65//! An attacker who knows q2 can forge a collision:
66//!
67//! ```text
68//! Message B (length 11):
69//!
70//! block 1 block 2 partial
71//! +--------------+ +--------------+ +-----------+
72//! | h0 h1 h2 h3 | | h4 h5 h6 h7 | | h8 h9 q2 |
73//! +--------------+ +--------------+ +-----------+
74//!
75//! Steps 1-2 are identical. Step 3 now has 3 elements:
76//! state = [h8, h9, q2, q3 | q4, q5, q6, q7] -> P -> digest
77//! ^^^^^^^^^^^^^^^^
78//! same state as Message A => same digest!
79//! ```
80//!
81//! In XOR-mode sponges this would be called 0-padding. In overwrite
82//! mode the leftover positions aren't zeros but old permutation
83//! output -- the effect is the same: no injective encoding of length.
84//!
85//! Note: this is only exploitable when the attacker controls the input
86//! length. When the length is fixed by the protocol (e.g. Merkle tree
87//! leaves), no collision is possible.
88//!
89//! The fix is 10-padding -- see `Pad10Sponge` for the full scheme.
90
91use alloc::string::String;
92use core::marker::PhantomData;
93use core::ops::Add;
94
95use itertools::Itertools;
96use p3_field::{
97 PrimeField, PrimeField32, absorb_radix_bits, max_shifted_absorb_injective_limbs,
98 reduce_packed_shifted,
99};
100
101use crate::Permutation;
102use crate::hasher::CryptographicHasher;
103use crate::permutation::{CryptographicPermutation, Derangement};
104
105/// A derangement d(x) = x + increment.
106///
107/// This is the standard padding function for sponge constructions.
108/// A derangement has no fixed points (d(x) != x for all x), which
109/// holds as long as the stored increment is non-zero.
110///
111/// ```ignore
112/// Increment(BabyBear::ONE) // d(x) = x + 1 for field elements
113/// Increment(1u64) // d(x) = x + 1 for raw integers
114/// ```
115#[derive(Copy, Clone, Debug)]
116pub struct Increment<T>(pub T);
117
118impl<T: Clone + Sync + Send + Add<Output = T>> Permutation<T> for Increment<T> {
119 fn permute(&self, input: T) -> T {
120 input + self.0.clone()
121 }
122}
123
124impl<T: Clone + Sync + Send + Add<Output = T>> Derangement<T> for Increment<T> {}
125
126/// A padding-free, overwrite-mode sponge.
127///
128/// # Security
129///
130/// Safe **only** for fixed-length inputs (e.g. Merkle leaves, trace
131/// rows). For variable-length inputs, use `Pad10Sponge`.
132///
133/// **Not** collision-resistant for variable-length inputs.
134/// Different-length messages can hash identically:
135///
136/// ```text
137/// RATE = 2
138/// [a] -> [a, 0 | cap...] -> P -> digest
139/// [a, 0] -> [a, 0 | cap...] -> P -> digest <- same!
140/// ```
141///
142/// # Parameters
143///
144/// - `WIDTH` -- total state size (rate + capacity).
145/// - `RATE` -- positions overwritten per block.
146/// - `OUT` -- elements squeezed from the final state.
147#[derive(Copy, Clone, Debug)]
148pub struct PaddingFreeSponge<P, const WIDTH: usize, const RATE: usize, const OUT: usize> {
149 /// The cryptographic permutation applied after each absorbed block.
150 permutation: P,
151}
152
153impl<P, const WIDTH: usize, const RATE: usize, const OUT: usize>
154 PaddingFreeSponge<P, WIDTH, RATE, OUT>
155{
156 pub const fn new(permutation: P) -> Self {
157 const {
158 assert!(RATE > 0);
159 assert!(RATE < WIDTH);
160 assert!(OUT <= WIDTH);
161 }
162 Self { permutation }
163 }
164}
165
166impl<T, P, const WIDTH: usize, const RATE: usize, const OUT: usize> CryptographicHasher<T, [T; OUT]>
167 for PaddingFreeSponge<P, WIDTH, RATE, OUT>
168where
169 T: Default + Copy,
170 P: CryptographicPermutation<[T; WIDTH]>,
171{
172 fn hash_iter<I>(&self, input: I) -> [T; OUT]
173 where
174 I: IntoIterator<Item = T>,
175 {
176 // Start from the all-zero state.
177 let mut state = [T::default(); WIDTH];
178 let mut input = input.into_iter();
179
180 'outer: loop {
181 // Absorb one block: overwrite state[0..RATE] with input elements one at a time.
182 for i in 0..RATE {
183 if let Some(x) = input.next() {
184 // Overwrite the i-th rate position.
185 state[i] = x;
186 } else {
187 // Input exhausted mid-block. Permute only if at least
188 // one element was absorbed in this block (i > 0).
189 // If i == 0 the state already reflects the previous
190 // permutation output and needs no extra call.
191 if i != 0 {
192 self.permutation.permute_mut(&mut state);
193 }
194 break 'outer;
195 }
196 }
197
198 // Full block absorbed. Permute before the next block.
199 self.permutation.permute_mut(&mut state);
200 }
201
202 // Squeeze: return the first OUT elements of the final state.
203 state[..OUT].try_into().unwrap()
204 }
205}
206
207/// An overwrite-mode sponge with 10-padding.
208///
209/// Absorbs input into the rate, permutes after each full block, and
210/// squeezes `OUT` elements. Two-case padding ensures collision
211/// resistance for inputs of **variable** length.
212///
213/// # Padding Rule
214///
215/// **Case 1 -- partial block** (input ends at position i < RATE):
216///
217/// ```text
218/// Sentinel at position i, zeros after, then permute.
219///
220/// [a] RATE=2: [a, S, 0, ... | cap...] -> P
221/// [a, 0] RATE=2: [a, 0, S, ... | cap...] -> P
222/// ^
223/// different position => no collision
224/// ```
225///
226/// **Case 2 -- full block** (input length is a multiple of RATE):
227///
228/// ```text
229/// Add sentinel to first capacity element, then permute.
230///
231/// [a, b] RATE=2: [a, b | cap_0 + S, cap_1, ...] -> P
232/// ```
233///
234/// Sentinel lands in rate (case 1) vs capacity (case 2), so no
235/// length-k input can collide with any length != k.
236///
237/// # Role of the Derangement
238///
239/// The padding function is a derangement d: a permutation with no
240/// fixed points (d(x) != x for all x). This guarantees:
241///
242/// - **Rate-domain**: d(0) != 0, so the sentinel is always non-zero.
243/// - **Capacity-domain**: d(state\[RATE\]) != state\[RATE\], so the
244/// capacity always changes.
245///
246/// ```text
247/// Partial: state[i] = d(0) -- sentinel
248/// Full: state[RATE] = d(state[RATE]) -- domain separator
249/// ```
250///
251/// # Construction
252///
253/// The padding function is a derangement (permutation with no fixed
254/// points). The standard choice is `Increment` which computes d(x) = x + 1:
255///
256/// ```ignore
257/// Pad10Sponge::new(permutation, Increment(BabyBear::ONE)) // field
258/// Pad10Sponge::new(permutation, Increment(1u64)) // integer
259/// ```
260///
261/// The derangement **must have no fixed points** (d(x) != x for all x).
262///
263/// # Parameters
264///
265/// - `WIDTH` -- total state size (rate + capacity).
266/// - `RATE` -- positions overwritten per block.
267/// - `OUT` -- elements squeezed from the final state.
268///
269/// # Security
270///
271/// Indifferentiable from a random oracle up to |F|^{c/2} queries (c = WIDTH - RATE).
272///
273/// Implies collision resistance, preimage resistance, etc. \[BDPA08\] + \[LBM25, Section 3.1\].
274#[derive(Debug)]
275pub struct Pad10Sponge<T, P, D, const WIDTH: usize, const RATE: usize, const OUT: usize> {
276 /// The cryptographic permutation applied after each absorbed block.
277 permutation: P,
278
279 /// A derangement (permutation with no fixed points) used for padding.
280 ///
281 /// - Rate-domain: `state[i] = d(T::default())`
282 /// - Capacity-domain: `state[RATE] = d(state[RATE])`
283 padding_derangement: D,
284
285 _phantom: PhantomData<T>,
286}
287
288impl<T, P: Clone, D: Clone, const WIDTH: usize, const RATE: usize, const OUT: usize> Clone
289 for Pad10Sponge<T, P, D, WIDTH, RATE, OUT>
290{
291 fn clone(&self) -> Self {
292 Self {
293 permutation: self.permutation.clone(),
294 padding_derangement: self.padding_derangement.clone(),
295 _phantom: PhantomData,
296 }
297 }
298}
299
300impl<T, P: Copy, D: Copy, const WIDTH: usize, const RATE: usize, const OUT: usize> Copy
301 for Pad10Sponge<T, P, D, WIDTH, RATE, OUT>
302{
303}
304
305impl<T, P, D, const WIDTH: usize, const RATE: usize, const OUT: usize>
306 Pad10Sponge<T, P, D, WIDTH, RATE, OUT>
307{
308 pub const fn new(permutation: P, padding_derangement: D) -> Self {
309 const {
310 assert!(RATE > 0);
311 assert!(RATE < WIDTH);
312 assert!(OUT <= WIDTH);
313 }
314 Self {
315 permutation,
316 padding_derangement,
317 _phantom: PhantomData,
318 }
319 }
320}
321
322impl<T, P, D, const WIDTH: usize, const RATE: usize, const OUT: usize>
323 CryptographicHasher<T, [T; OUT]> for Pad10Sponge<T, P, D, WIDTH, RATE, OUT>
324where
325 T: Default + Copy,
326 P: CryptographicPermutation<[T; WIDTH]>,
327 D: Derangement<T>,
328{
329 fn hash_iter<I>(&self, input: I) -> [T; OUT]
330 where
331 I: IntoIterator<Item = T>,
332 {
333 // Start from the all-zero state.
334 let mut state = [T::default(); WIDTH];
335
336 // Wrap the iterator in `peekable()`.
337 //
338 // We can detect when input is exhausted on a block boundary without consuming past it.
339 let mut input = input.into_iter().peekable();
340
341 loop {
342 // Absorb phase: overwrite state[0..RATE] one element at a time.
343 //
344 // If the iterator runs dry mid-block we enter partial-block padding immediately.
345 for i in 0..RATE {
346 if let Some(x) = input.next() {
347 // Overwrite the i-th rate position with the next input element.
348 state[i] = x;
349 } else {
350 // Partial block: rate-domain 10*-padding.
351 // position i <- d(0) (the sentinel)
352 // positions i+1.. <- zero (the "0*" suffix)
353 //
354 // [a] RATE=3 -> [a, d(0), 0 | cap...]
355 // [a, b] RATE=3 -> [a, b, d(0) | cap...]
356 state[i] = self.padding_derangement.permute(T::default());
357 for s in state.iter_mut().take(RATE).skip(i + 1) {
358 *s = T::default();
359 }
360
361 // Permute the padded state and squeeze.
362 self.permutation.permute_mut(&mut state);
363 return state[..OUT].try_into().unwrap();
364 }
365 }
366
367 // Full block absorbed. Check whether more input follows.
368 if input.peek().is_none() {
369 // Capacity-domain padding: apply derangement to state[RATE].
370 //
371 // Why derangement (not overwrite)?
372 // - Overwriting would leak a relation between sponge(M)
373 // and sponge(M || 0^RATE) via multi-block squeeze.
374 // - The derangement preserves accumulated capacity
375 // while injecting the domain separator [LBM25].
376 state[RATE] = self.padding_derangement.permute(state[RATE]);
377
378 // Permute the padded state and squeeze.
379 self.permutation.permute_mut(&mut state);
380 return state[..OUT].try_into().unwrap();
381 }
382
383 // More input to come. Permute and continue to the next block.
384 self.permutation.permute_mut(&mut state);
385 }
386 }
387}
388
389/// Padding-free sponge over a large prime field, accepting 32-bit field elements as input.
390///
391/// # Security
392///
393/// **Not** collision-resistant for variable-length inputs.
394///
395/// For variable-length inputs, use [`MultiField32Pad10Sponge`].
396#[derive(Clone, Debug)]
397pub struct MultiField32PaddingFreeSponge<
398 F,
399 PF,
400 P,
401 const WIDTH: usize,
402 const RATE: usize,
403 const OUT: usize,
404> {
405 /// The cryptographic permutation applied after each absorbed block.
406 permutation: P,
407 /// How many small-field elements fit inside one large-field element.
408 num_f_elms: usize,
409 /// Radix used for shifted packing into the large field.
410 radix_bits: u32,
411 _phantom: PhantomData<(F, PF)>,
412}
413
414impl<F, PF, P, const WIDTH: usize, const RATE: usize, const OUT: usize>
415 MultiField32PaddingFreeSponge<F, PF, P, WIDTH, RATE, OUT>
416where
417 F: PrimeField32,
418 PF: PrimeField,
419{
420 pub fn new(permutation: P) -> Result<Self, String> {
421 const {
422 assert!(RATE > 0);
423 assert!(RATE < WIDTH);
424 assert!(OUT <= WIDTH);
425 }
426 if F::order() >= PF::order() {
427 return Err(String::from("F::order() must be less than PF::order()"));
428 }
429
430 // Use shifted-radix injective packing for robust absorb encoding.
431 let num_f_elms = max_shifted_absorb_injective_limbs::<F, PF>();
432 let radix_bits = absorb_radix_bits::<F>();
433 Ok(Self {
434 permutation,
435 num_f_elms,
436 radix_bits,
437 _phantom: PhantomData,
438 })
439 }
440}
441
442impl<F, PF, P, const WIDTH: usize, const RATE: usize, const OUT: usize>
443 CryptographicHasher<F, [PF; OUT]> for MultiField32PaddingFreeSponge<F, PF, P, WIDTH, RATE, OUT>
444where
445 F: PrimeField32,
446 PF: PrimeField + Default + Copy,
447 P: CryptographicPermutation<[PF; WIDTH]>,
448{
449 fn hash_iter<I>(&self, input: I) -> [PF; OUT]
450 where
451 I: IntoIterator<Item = F>,
452 {
453 const {
454 assert!(RATE > 0);
455 assert!(RATE < WIDTH);
456 assert!(OUT <= WIDTH);
457 }
458 let mut state = [PF::default(); WIDTH];
459
460 // Example: RATE = 3, num_f_elms = 2, input = [f0..f7]
461 //
462 // block_chunk = [f0, f1, f2, f3, f4, f5] (RATE * 2 = 6 small elems)
463 // chunk 0: [f0, f1] -> pack into PF -> state[0]
464 // chunk 1: [f2, f3] -> pack into PF -> state[1]
465 // chunk 2: [f4, f5] -> pack into PF -> state[2]
466 // -> permute
467 //
468 // block_chunk = [f6, f7] (partial)
469 // chunk 0: [f6, f7] -> pack into PF -> state[0]
470 // -> permute
471 for block_chunk in &input.into_iter().chunks(RATE * self.num_f_elms) {
472 for (chunk_id, chunk) in (&block_chunk.chunks(self.num_f_elms))
473 .into_iter()
474 .enumerate()
475 {
476 // Pack num_f_elms small-field elements into one large-field
477 // element via shifted-radix reduction.
478 state[chunk_id] = reduce_packed_shifted(&chunk.collect_vec(), self.radix_bits);
479 }
480 state = self.permutation.permute(state);
481 }
482
483 state[..OUT].try_into().unwrap()
484 }
485}
486
487/// 10-padded sponge over a large prime field, accepting 32-bit field elements as input.
488///
489/// # Data Flow
490///
491/// ```text
492/// Small-field input: [f0, f1, f2, f3, f4, f5, ...]
493/// \___/ \___/ \___/
494/// pack into pack into pack into
495/// state[0] state[1] state[2]
496/// ---- one large-field block ---- -> P
497/// ```
498///
499/// # Padding
500///
501/// Same two-case scheme as [`Pad10Sponge`], applied in the large-field
502/// domain using the multiplicative identity as sentinel.
503///
504/// # Security
505///
506/// Collision-resistant for variable-length inputs.
507#[derive(Clone, Debug)]
508pub struct MultiField32Pad10Sponge<
509 F,
510 PF,
511 P,
512 const WIDTH: usize,
513 const RATE: usize,
514 const OUT: usize,
515> {
516 /// The cryptographic permutation applied after each absorbed block.
517 permutation: P,
518 /// Packing ratio: how many small-field elements fit in one large-field element.
519 ///
520 /// E.g. 64-bit field / 32-bit field = 2.
521 num_f_elms: usize,
522 /// Radix used for shifted packing into the large field.
523 radix_bits: u32,
524 _phantom: PhantomData<(F, PF)>,
525}
526
527impl<F, PF, P, const WIDTH: usize, const RATE: usize, const OUT: usize>
528 MultiField32Pad10Sponge<F, PF, P, WIDTH, RATE, OUT>
529where
530 F: PrimeField32,
531 PF: PrimeField,
532{
533 pub fn new(permutation: P) -> Result<Self, String> {
534 const {
535 assert!(RATE > 0);
536 assert!(RATE < WIDTH);
537 assert!(OUT <= WIDTH);
538 }
539 if F::order() >= PF::order() {
540 return Err(String::from("F::order() must be less than PF::order()"));
541 }
542
543 // Use shifted-radix injective packing for robust absorb encoding.
544 let num_f_elms = max_shifted_absorb_injective_limbs::<F, PF>();
545 let radix_bits = absorb_radix_bits::<F>();
546 Ok(Self {
547 permutation,
548 num_f_elms,
549 radix_bits,
550 _phantom: PhantomData,
551 })
552 }
553}
554
555impl<F, PF, P, const WIDTH: usize, const RATE: usize, const OUT: usize>
556 CryptographicHasher<F, [PF; OUT]> for MultiField32Pad10Sponge<F, PF, P, WIDTH, RATE, OUT>
557where
558 F: PrimeField32,
559 PF: PrimeField + Default + Copy,
560 P: CryptographicPermutation<[PF; WIDTH]>,
561{
562 fn hash_iter<I>(&self, input: I) -> [PF; OUT]
563 where
564 I: IntoIterator<Item = F>,
565 {
566 // All-zero initial state in the large-field domain.
567 let mut state = [PF::default(); WIDTH];
568
569 // The padding sentinel: multiplicative identity in the large field.
570 let sentinel = PF::ONE;
571
572 // Tracks how many large-field rate slots the current block filled.
573 //
574 // After the loop:
575 // last_chunk_len = 0 && absorbed_any = true -> full-block case
576 // last_chunk_len = 0 && absorbed_any = false -> empty input
577 // last_chunk_len > 0 -> partial block
578 let mut last_chunk_len = 0;
579 let mut absorbed_any = false;
580
581 // Outer loop: consume RATE * num_f_elms small-field elements per iteration.
582 //
583 // That fills exactly RATE large-field rate slots.
584 //
585 // Example: RATE = 3, num_f_elms = 2
586 //
587 // iter 1: [f0..f5] -> state = [pack(f0,f1), pack(f2,f3), pack(f4,f5), cap...]
588 // full block (3 = RATE) -> permute, reset last_chunk_len = 0
589 //
590 // iter 2: [f6..f9] -> state = [pack(f6,f7), pack(f8,f9), old, cap...]
591 // partial (2 < RATE) -> skip permute, pad below
592 for block_chunk in &input.into_iter().chunks(RATE * self.num_f_elms) {
593 absorbed_any = true;
594 last_chunk_len = 0;
595
596 // Inner loop:
597 // - group num_f_elms small-field elements,
598 // - pack each group into one large-field element at the next rate slot.
599 for (chunk_id, chunk) in (&block_chunk.chunks(self.num_f_elms))
600 .into_iter()
601 .enumerate()
602 {
603 // Shifted-radix reduction: num_f_elms small -> 1 large.
604 state[chunk_id] = reduce_packed_shifted(&chunk.collect_vec(), self.radix_bits);
605
606 // Record how far we got (1-indexed).
607 last_chunk_len = chunk_id + 1;
608 }
609
610 // Only permute when the block is full.
611 // Partial blocks fall through to the padding logic below.
612 if last_chunk_len == RATE {
613 state = self.permutation.permute(state);
614 last_chunk_len = 0;
615 }
616 }
617
618 // Two-case padding in the large-field domain.
619 //
620 // last_chunk_len = 0 + absorbed -> capacity pad: state[RATE] += 1
621 // last_chunk_len > 0 or empty -> rate pad: state[pos] = 1, zeros after
622 if last_chunk_len == 0 && absorbed_any {
623 // Full block: add sentinel to first capacity element.
624 state[RATE] += sentinel;
625 } else {
626 // Partial block or empty: sentinel at next open rate slot.
627 state[last_chunk_len] = sentinel;
628
629 // Zero-fill remaining rate slots (the "0*" suffix).
630 for s in state.iter_mut().take(RATE).skip(last_chunk_len + 1) {
631 *s = PF::default();
632 }
633 }
634
635 // Permute the padded state.
636 state = self.permutation.permute(state);
637
638 // Squeeze the first OUT large-field elements.
639 state[..OUT].try_into().unwrap()
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use p3_field::PrimeCharacteristicRing;
646 use p3_koala_bear::KoalaBear;
647 use proptest::prelude::*;
648
649 use super::*;
650 use crate::Permutation;
651
652 #[derive(Clone)]
653 struct MockPermutation;
654
655 impl<T, const WIDTH: usize> Permutation<[T; WIDTH]> for MockPermutation
656 where
657 T: Copy + core::ops::Add<Output = T> + Default,
658 {
659 fn permute_mut(&self, input: &mut [T; WIDTH]) {
660 // Sum all elements and broadcast.
661 let sum: T = input.iter().copied().fold(T::default(), |acc, x| acc + x);
662 // Set every element to the sum
663 *input = [sum; WIDTH];
664 }
665 }
666
667 impl<T, const WIDTH: usize> CryptographicPermutation<[T; WIDTH]> for MockPermutation where
668 T: Copy + core::ops::Add<Output = T> + Default
669 {
670 }
671
672 /// Mock: weighted sum. output[i] = sum_j state[j] * (j+1).
673 ///
674 /// Position-sensitive: sentinel position affects the output.
675 #[derive(Clone)]
676 struct WeightedSumPermutation;
677
678 impl<const WIDTH: usize> Permutation<[KoalaBear; WIDTH]> for WeightedSumPermutation {
679 fn permute_mut(&self, input: &mut [KoalaBear; WIDTH]) {
680 // Weighted sum: element j contributes input[j] * (j + 1).
681 let weighted_sum: KoalaBear = input
682 .iter()
683 .enumerate()
684 .map(|(j, &x)| x * KoalaBear::new((j + 1) as u32))
685 .fold(KoalaBear::ZERO, |a, b| a + b);
686
687 // Broadcast the weighted sum to every position.
688 *input = [weighted_sum; WIDTH];
689 }
690 }
691
692 impl<const WIDTH: usize> CryptographicPermutation<[KoalaBear; WIDTH]> for WeightedSumPermutation {}
693
694 #[test]
695 fn test_padding_free_sponge_basic() {
696 // Fixture: WIDTH = 4, RATE = 2, OUT = 2, plain-sum mock.
697 //
698 // Step-by-step absorption of [1, 2, 3, 4, 5]:
699 //
700 // Initial state: [0, 0, 0, 0]
701 //
702 // Block 1: overwrite rate [1, 2, 0, 0]
703 // Permute (sum = 3): [3, 3, 3, 3]
704 //
705 // Block 2: overwrite rate [3, 4, 3, 3]
706 // Permute (sum = 13): [13, 13, 13, 13]
707 //
708 // Partial block: overwrite [5] [5, 13, 13, 13]
709 // Permute (sum = 44): [44, 44, 44, 44]
710 //
711 // Squeeze OUT = 2: [44, 44]
712 const WIDTH: usize = 4;
713 const RATE: usize = 2;
714 const OUT: usize = 2;
715
716 let sponge = PaddingFreeSponge::<MockPermutation, WIDTH, RATE, OUT>::new(MockPermutation);
717
718 let input = [1, 2, 3, 4, 5];
719 let output = sponge.hash_iter(input);
720
721 assert_eq!(output, [44; OUT]);
722 }
723
724 #[test]
725 fn test_padding_free_sponge_empty_input() {
726 // Empty input: no elements absorbed, no permutation called.
727 //
728 // The initial all-zero state is returned directly.
729 const WIDTH: usize = 4;
730 const RATE: usize = 2;
731 const OUT: usize = 2;
732
733 let sponge = PaddingFreeSponge::<MockPermutation, WIDTH, RATE, OUT>::new(MockPermutation);
734
735 let input: [u64; 0] = [];
736 let output = sponge.hash_iter(input);
737
738 // Squeeze from the untouched zero state.
739 assert_eq!(output, [0; OUT]);
740 }
741
742 #[test]
743 fn test_padding_free_sponge_exact_block_size() {
744 // Fixture: WIDTH = 6, RATE = 3, OUT = 2, plain-sum mock.
745 //
746 // Input [10, 20, 30] fills exactly one block:
747 //
748 // Block 1: overwrite rate [10, 20, 30, 0, 0, 0]
749 // Permute (sum = 60): [60, 60, 60, 60, 60, 60]
750 //
751 // Squeeze OUT = 2: [60, 60]
752 const WIDTH: usize = 6;
753 const RATE: usize = 3;
754 const OUT: usize = 2;
755
756 let sponge = PaddingFreeSponge::<MockPermutation, WIDTH, RATE, OUT>::new(MockPermutation);
757
758 let input = [10, 20, 30];
759 let output = sponge.hash_iter(input);
760
761 assert_eq!(output, [60; OUT]);
762 }
763
764 #[test]
765 fn test_pad10_no_collision_partial_vs_trailing_zero() {
766 // Invariant: [a] != [a, 0].
767 //
768 // [a] -> partial pad -> [42, 1, 0, 0] wt_sum = 44
769 // [a, 0] -> full block -> [42, 0, 1, 0] wt_sum = 45
770 // ^ sentinel at different pos
771 let sponge =
772 Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
773 WeightedSumPermutation,
774 Increment(KoalaBear::ONE),
775 );
776
777 let a = KoalaBear::new(42);
778
779 let hash_short = sponge.hash_iter([a]);
780 let hash_long = sponge.hash_iter([a, KoalaBear::ZERO]);
781
782 assert_ne!(hash_short, hash_long);
783 }
784
785 #[test]
786 fn test_pad10_no_collision_full_block_vs_extended() {
787 // Invariant: [a, b] (1 full block) != [a, b, c] (1.5 blocks).
788 //
789 // [a, b] -> capacity pad -> [1, 2 | 0+1, 0] -> P
790 // [a, b, c] -> P([1, 2, 0, 0]) -> partial [5, 1, *, *] -> P
791 let sponge =
792 Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
793 WeightedSumPermutation,
794 Increment(KoalaBear::ONE),
795 );
796
797 let a = KoalaBear::new(1);
798 let b = KoalaBear::new(2);
799 let c = KoalaBear::new(5);
800
801 let hash_full = sponge.hash_iter([a, b]);
802 let hash_ext = sponge.hash_iter([a, b, c]);
803
804 assert_ne!(hash_full, hash_ext);
805 }
806
807 #[test]
808 fn test_pad10_empty_input_is_nontrivial() {
809 // Empty input -> partial pad at position 0.
810 //
811 // state = [1, 0, 0, 0] -> wt_sum = 1
812 //
813 // Digest must NOT be the all-zero default.
814 let sponge =
815 Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
816 WeightedSumPermutation,
817 Increment(KoalaBear::ONE),
818 );
819
820 let output = sponge.hash_iter(core::iter::empty::<KoalaBear>());
821
822 assert_eq!(output, [KoalaBear::ONE; 2]);
823 }
824
825 #[test]
826 fn test_pad10_basic_absorption() {
827 // Fixture: WIDTH = 4, RATE = 2, OUT = 2, weighted-sum mock.
828 //
829 // Step-by-step absorption of [1, 2, 3, 4, 5]:
830 //
831 // Initial state: [0, 0, 0, 0]
832 //
833 // Block 1: overwrite rate [1, 2, 0, 0]
834 // (peek: more input)
835 // Weighted sum = 1*1 + 2*2 + 0 + 0 = 5
836 // Permute: [5, 5, 5, 5]
837 //
838 // Block 2: overwrite rate [3, 4, 5, 5]
839 // (peek: more input)
840 // Weighted sum = 3*1 + 4*2 + 5*3 + 5*4 = 46
841 // Permute: [46, 46, 46, 46]
842 //
843 // Partial block: overwrite position 0 [5, 46, 46, 46]
844 // Rate-domain pad at position 1: [5, 1, 46, 46]
845 // ^ ^ sentinel
846 //
847 // Weighted sum = 5*1 + 1*2 + 46*3 + 46*4 = 329
848 //
849 // Permute: [329, 329, 329, 329]
850 //
851 // Squeeze OUT = 2: [329, 329]
852 let sponge =
853 Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
854 WeightedSumPermutation,
855 Increment(KoalaBear::ONE),
856 );
857
858 let input = [1u32, 2, 3, 4, 5].map(KoalaBear::new);
859 let output = sponge.hash_iter(input);
860
861 assert_eq!(output, [KoalaBear::new(329); 2]);
862 }
863
864 #[test]
865 fn test_pad10_exact_block_uses_capacity_padding() {
866 // Fixture: WIDTH = 6, RATE = 3, OUT = 2, weighted-sum mock.
867 //
868 // Input [10, 20, 30] fills exactly one block:
869 //
870 // Block 1: overwrite rate [10, 20, 30, 0, 0, 0]
871 // (peek: no more input -> capacity pad)
872 // state[RATE] += 1 -> state[3] += 1: [10, 20, 30, 1, 0, 0]
873 //
874 // Weighted sum = 10*1 + 20*2 + 30*3 + 1*4 + 0 + 0 = 144
875 // Permute: [144; 6]
876 //
877 // Squeeze OUT = 2: [144, 144]
878 let sponge =
879 Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 6, 3, 2>::new(
880 WeightedSumPermutation,
881 Increment(KoalaBear::ONE),
882 );
883
884 let input = [10u32, 20, 30].map(KoalaBear::new);
885 let output = sponge.hash_iter(input);
886
887 assert_eq!(output, [KoalaBear::new(144); 2]);
888 }
889
890 // Arbitrary field element from any u32.
891 fn arb_koala_bear() -> impl Strategy<Value = KoalaBear> {
892 any::<u32>().prop_map(KoalaBear::new)
893 }
894
895 proptest! {
896 #[test]
897 fn prop_pad10_different_lengths_never_collide(
898 // Generate a random base input of 1..=8 elements.
899 base in prop::collection::vec(arb_koala_bear(), 1..=8)
900 ) {
901 // Invariant: hash(msg) != hash(msg ++ [0]).
902 //
903 // This is the exact attack that padding prevents.
904 let sponge = Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
905 WeightedSumPermutation,
906 Increment(KoalaBear::ONE),
907 );
908
909 // Hash the base message.
910 let hash_base = sponge.hash_iter(base.iter().copied());
911
912 // Extend by one zero element and hash again.
913 let mut extended = base.clone();
914 extended.push(KoalaBear::ZERO);
915 let hash_extended = sponge.hash_iter(extended.iter().copied());
916
917 // The two digests must differ.
918 prop_assert_ne!(
919 hash_base,
920 hash_extended,
921 "base len={}, extended len={}",
922 base.len(),
923 base.len() + 1
924 );
925 }
926
927 #[test]
928 fn prop_pad10_deterministic(
929 // Generate a random input of 0..=12 elements.
930 input in prop::collection::vec(arb_koala_bear(), 0..=12)
931 ) {
932 // Invariant: hash(x) == hash(x). No hidden mutable state.
933 let sponge = Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
934 WeightedSumPermutation,
935 Increment(KoalaBear::ONE),
936 );
937
938 // Hash the input twice with independent iterator clones.
939 let hash_1 = sponge.hash_iter(input.iter().copied());
940 let hash_2 = sponge.hash_iter(input.iter().copied());
941
942 prop_assert_eq!(hash_1, hash_2);
943 }
944
945 #[test]
946 fn prop_pad10_prefix_differs_from_full(
947 // Generate a random input of 2..=8 elements.
948 input in prop::collection::vec(arb_koala_bear(), 2..=8)
949 ) {
950 // Invariant: hash(input[..k]) != hash(input) for all k < len.
951 //
952 // Tests collision resistance across arbitrary length gaps.
953 let sponge = Pad10Sponge::<KoalaBear, WeightedSumPermutation, Increment<KoalaBear>, 4, 2, 2>::new(
954 WeightedSumPermutation,
955 Increment(KoalaBear::ONE),
956 );
957
958 // Hash the full input.
959 let hash_full = sponge.hash_iter(input.iter().copied());
960
961 // Hash every strict prefix and verify distinctness.
962 for k in 1..input.len() {
963 let hash_prefix = sponge.hash_iter(input[..k].iter().copied());
964 prop_assert_ne!(
965 hash_full,
966 hash_prefix,
967 "prefix len={} collides with full len={}",
968 k,
969 input.len()
970 );
971 }
972 }
973 }
974}