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}