Skip to main content

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}