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