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}