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