Skip to main content

miden_crypto/hash/algebraic_sponge/rescue/rpx/
mod.rs

1use super::{
2    ARK1, ARK2, AlgebraicSponge, CAPACITY_RANGE, DIGEST_RANGE, Felt, MDS, NUM_ROUNDS, RATE_RANGE,
3    RATE0_RANGE, RATE1_RANGE, Range, STATE_WIDTH, Word, add_constants,
4    add_constants_and_apply_ext_round, add_constants_and_apply_inv_sbox,
5    add_constants_and_apply_sbox, apply_inv_sbox, apply_mds, apply_sbox,
6};
7
8#[cfg(test)]
9mod tests;
10
11// HASHER IMPLEMENTATION
12// ================================================================================================
13
14/// Implementation of the Rescue Prime eXtension hash function with 256-bit output.
15///
16/// The hash function is based on the XHash12 construction in [specifications](https://eprint.iacr.org/2023/1045)
17///
18/// The parameters used to instantiate the function are:
19/// * Field: 64-bit prime field with modulus 2^64 - 2^32 + 1.
20/// * State width: 12 field elements.
21/// * Capacity size: 4 field elements.
22/// * S-Box degree: 7.
23/// * Rounds: There are 3 different types of rounds:
24/// - (FB): `apply_mds` → `add_constants` → `apply_sbox` → `apply_mds` → `add_constants` →
25///   `apply_inv_sbox`.
26/// - (E): `add_constants` → `ext_sbox` (which is raising to power 7 in the degree 3 extension
27///   field).
28/// - (M): `apply_mds` → `add_constants`.
29/// * Permutation: (FB) (E) (FB) (E) (FB) (E) (M).
30///
31/// The above parameters target a 128-bit security level. The digest consists of four field elements
32/// and it can be serialized into 32 bytes (256 bits).
33///
34/// ## Hash output consistency
35/// Functions [hash_elements()](Rpx256::hash_elements), [merge()](Rpx256::merge), and
36/// [merge_with_int()](Rpx256::merge_with_int) are internally consistent. That is, computing
37/// a hash for the same set of elements using these functions will always produce the same
38/// result. For example, merging two digests using [merge()](Rpx256::merge) will produce the
39/// same result as hashing 8 elements which make up these digests using
40/// [hash_elements()](Rpx256::hash_elements) function.
41///
42/// However, [hash()](Rpx256::hash) function is not consistent with functions mentioned above.
43/// For example, if we take two field elements, serialize them to bytes and hash them using
44/// [hash()](Rpx256::hash), the result will differ from the result obtained by hashing these
45/// elements directly using [hash_elements()](Rpx256::hash_elements) function. The reason for
46/// this difference is that [hash()](Rpx256::hash) function needs to be able to handle
47/// arbitrary binary strings, which may or may not encode valid field elements - and thus,
48/// deserialization procedure used by this function is different from the procedure used to
49/// deserialize valid field elements.
50///
51/// Thus, if the underlying data consists of valid field elements, it might make more sense
52/// to deserialize them into field elements and then hash them using
53/// [hash_elements()](Rpx256::hash_elements) function rather than hashing the serialized bytes
54/// using [hash()](Rpx256::hash) function.
55///
56/// ## Domain separation
57/// [merge_in_domain()](Rpx256::merge_in_domain) hashes two digests into one digest with some domain
58/// identifier and the current implementation sets the second capacity element to the value of
59/// this domain identifier. Using a similar argument to the one formulated for domain separation
60/// in Appendix C of the [specifications](https://eprint.iacr.org/2023/1045), one sees that doing
61/// so degrades only pre-image resistance, from its initial bound of c.log_2(p), by as much as
62/// the log_2 of the size of the domain identifier space. Since pre-image resistance becomes
63/// the bottleneck for the security bound of the sponge in overwrite-mode only when it is
64/// lower than 2^128, we see that the target 128-bit security level is maintained as long as
65/// the size of the domain identifier space, including for padding, is less than 2^128.
66///
67/// ## Hashing of empty input
68/// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has
69/// the benefit of requiring no calls to the RPX permutation when hashing empty input.
70#[allow(rustdoc::private_intra_doc_links)]
71#[derive(Debug, Copy, Clone, Eq, PartialEq)]
72pub struct Rpx256();
73
74impl AlgebraicSponge for Rpx256 {
75    /// Applies RPX permutation to the provided state.
76    #[inline(always)]
77    fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) {
78        Self::apply_fb_round(state, 0);
79        Self::apply_ext_round(state, 1);
80        Self::apply_fb_round(state, 2);
81        Self::apply_ext_round(state, 3);
82        Self::apply_fb_round(state, 4);
83        Self::apply_ext_round(state, 5);
84        Self::apply_final_round(state, 6);
85    }
86}
87
88impl Rpx256 {
89    // CONSTANTS
90    // --------------------------------------------------------------------------------------------
91
92    /// Target collision resistance level in bits.
93    pub const COLLISION_RESISTANCE: u32 = 128;
94
95    /// Sponge state is set to 12 field elements or 768 bytes; 8 elements are reserved for the
96    /// rate and the remaining 4 elements are reserved for the capacity.
97    pub const STATE_WIDTH: usize = STATE_WIDTH;
98
99    /// The rate portion of the state is located in elements 0 through 7 (inclusive).
100    pub const RATE_RANGE: Range<usize> = RATE_RANGE;
101
102    /// The first 4-element word of the rate portion.
103    pub const RATE0_RANGE: Range<usize> = RATE0_RANGE;
104
105    /// The second 4-element word of the rate portion.
106    pub const RATE1_RANGE: Range<usize> = RATE1_RANGE;
107
108    /// The capacity portion of the state is located in elements 8, 9, 10, and 11.
109    pub const CAPACITY_RANGE: Range<usize> = CAPACITY_RANGE;
110
111    /// The output of the hash function can be read from state elements 0, 1, 2, and 3 (the first
112    /// word of the state).
113    pub const DIGEST_RANGE: Range<usize> = DIGEST_RANGE;
114
115    /// MDS matrix used for computing the linear layer in the (FB) and (E) rounds.
116    pub const MDS: [[Felt; STATE_WIDTH]; STATE_WIDTH] = MDS;
117
118    /// Round constants added to the hasher state in the first half of the round.
119    pub const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = ARK1;
120
121    /// Round constants added to the hasher state in the second half of the round.
122    pub const ARK2: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = ARK2;
123
124    // HASH FUNCTIONS
125    // --------------------------------------------------------------------------------------------
126
127    /// Returns a hash of the provided sequence of bytes.
128    #[inline(always)]
129    pub fn hash(bytes: &[u8]) -> Word {
130        <Self as AlgebraicSponge>::hash(bytes)
131    }
132
133    /// Returns a hash of the provided field elements.
134    #[inline(always)]
135    pub fn hash_elements<E: crate::field::BasedVectorSpace<Felt>>(elements: &[E]) -> Word {
136        <Self as AlgebraicSponge>::hash_elements(elements)
137    }
138
139    /// Returns a hash of two digests. This method is intended for use in construction of
140    /// Merkle trees and verification of Merkle paths.
141    #[inline(always)]
142    pub fn merge(values: &[Word; 2]) -> Word {
143        <Self as AlgebraicSponge>::merge(values)
144    }
145
146    /// Returns a hash of multiple digests.
147    #[inline(always)]
148    pub fn merge_many(values: &[Word]) -> Word {
149        <Self as AlgebraicSponge>::merge_many(values)
150    }
151
152    /// Returns a hash of a digest and a u64 value.
153    #[inline(always)]
154    pub fn merge_with_int(seed: Word, value: u64) -> Word {
155        <Self as AlgebraicSponge>::merge_with_int(seed, value)
156    }
157
158    /// Returns a hash of two digests and a domain identifier.
159    #[inline(always)]
160    pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word {
161        <Self as AlgebraicSponge>::merge_in_domain(values, domain)
162    }
163
164    // RPX PERMUTATION
165    // --------------------------------------------------------------------------------------------
166
167    /// Applies RPX permutation to the provided state.
168    #[inline(always)]
169    pub fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) {
170        Self::apply_fb_round(state, 0);
171        Self::apply_ext_round(state, 1);
172        Self::apply_fb_round(state, 2);
173        Self::apply_ext_round(state, 3);
174        Self::apply_fb_round(state, 4);
175        Self::apply_ext_round(state, 5);
176        Self::apply_final_round(state, 6);
177    }
178
179    // RPX PERMUTATION ROUND FUNCTIONS
180    // --------------------------------------------------------------------------------------------
181
182    /// (FB) round function.
183    #[inline(always)]
184    pub fn apply_fb_round(state: &mut [Felt; STATE_WIDTH], round: usize) {
185        apply_mds(state);
186        if !add_constants_and_apply_sbox(state, &ARK1[round]) {
187            add_constants(state, &ARK1[round]);
188            apply_sbox(state);
189        }
190
191        apply_mds(state);
192        if !add_constants_and_apply_inv_sbox(state, &ARK2[round]) {
193            add_constants(state, &ARK2[round]);
194            apply_inv_sbox(state);
195        }
196    }
197
198    /// (E) round function.
199    ///
200    /// It first attempts to run the optimized (SIMD-accelerated) implementation.
201    /// If SIMD acceleration is not available for the current target it falls
202    /// back to the scalar reference implementation (`apply_ext_round_ref`).
203    #[inline(always)]
204    pub fn apply_ext_round(state: &mut [Felt; STATE_WIDTH], round: usize) {
205        if !add_constants_and_apply_ext_round(state, &ARK1[round]) {
206            Self::apply_ext_round_ref(state, round);
207        }
208    }
209
210    /// Scalar (reference) implementation of the (E) round function.
211    ///
212    /// This version performs the round without SIMD acceleration and is used
213    /// as a fallback when optimized implementations are not available.
214    #[inline(always)]
215    fn apply_ext_round_ref(state: &mut [Felt; STATE_WIDTH], round: usize) {
216        // add constants
217        add_constants(state, &ARK1[round]);
218
219        // decompose the state into 4 elements in the cubic extension field and apply the power 7
220        // map to each of the elements using our custom cubic extension implementation
221        let [s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11] = *state;
222
223        let ext0 = cubic_ext::power7([s0, s1, s2]);
224        let ext1 = cubic_ext::power7([s3, s4, s5]);
225        let ext2 = cubic_ext::power7([s6, s7, s8]);
226        let ext3 = cubic_ext::power7([s9, s10, s11]);
227
228        // write the results back into the state
229        state[0] = ext0[0];
230        state[1] = ext0[1];
231        state[2] = ext0[2];
232        state[3] = ext1[0];
233        state[4] = ext1[1];
234        state[5] = ext1[2];
235        state[6] = ext2[0];
236        state[7] = ext2[1];
237        state[8] = ext2[2];
238        state[9] = ext3[0];
239        state[10] = ext3[1];
240        state[11] = ext3[2];
241    }
242
243    /// (M) round function.
244    #[inline(always)]
245    pub fn apply_final_round(state: &mut [Felt; STATE_WIDTH], round: usize) {
246        apply_mds(state);
247        add_constants(state, &ARK1[round]);
248    }
249}
250
251// CUBIC EXTENSION FIELD OPERATIONS
252// ================================================================================================
253
254/// Helper functions for cubic extension field operations over the irreducible polynomial
255/// x³ - x - 1. These are used for Plonky3 integration where we need explicit control
256/// over the field arithmetic.
257mod cubic_ext {
258    use super::Felt;
259    use crate::field::PrimeCharacteristicRing;
260
261    /// Multiplies two cubic extension field elements.
262    ///
263    /// Element representation: [a0, a1, a2] = a0 + a1*φ + a2*φ²
264    /// where φ is a root of x³ - x - 1.
265    #[inline(always)]
266    pub fn mul(a: [Felt; 3], b: [Felt; 3]) -> [Felt; 3] {
267        let a0b0 = a[0] * b[0];
268        let a1b1 = a[1] * b[1];
269        let a2b2 = a[2] * b[2];
270
271        let a0b0_a0b1_a1b0_a1b1 = (a[0] + a[1]) * (b[0] + b[1]);
272        let a0b0_a0b2_a2b0_a2b2 = (a[0] + a[2]) * (b[0] + b[2]);
273        let a1b1_a1b2_a2b1_a2b2 = (a[1] + a[2]) * (b[1] + b[2]);
274
275        let a0b0_minus_a1b1 = a0b0 - a1b1;
276
277        let a0b0_a1b2_a2b1 = a1b1_a1b2_a2b1_a2b2 + a0b0_minus_a1b1 - a2b2;
278        let a0b1_a1b0_a1b2_a2b1_a2b2 =
279            a0b0_a0b1_a1b0_a1b1 + a1b1_a1b2_a2b1_a2b2 - a1b1.double() - a0b0;
280        let a0b2_a1b1_a2b0_a2b2 = a0b0_a0b2_a2b0_a2b2 - a0b0_minus_a1b1;
281
282        [a0b0_a1b2_a2b1, a0b1_a1b0_a1b2_a2b1_a2b2, a0b2_a1b1_a2b0_a2b2]
283    }
284
285    /// Squares a cubic extension field element.
286    #[inline(always)]
287    pub fn square(a: [Felt; 3]) -> [Felt; 3] {
288        let a0 = a[0];
289        let a1 = a[1];
290        let a2 = a[2];
291
292        let a2_sq = a2.square();
293        let a1_a2 = a1 * a2;
294
295        let out0 = a0.square() + a1_a2.double();
296        let out1 = (a0 * a1 + a1_a2).double() + a2_sq;
297        let out2 = (a0 * a2).double() + a1.square() + a2_sq;
298
299        [out0, out1, out2]
300    }
301
302    /// Computes the 7th power of a cubic extension field element.
303    ///
304    /// Uses the addition chain: x → x² → x³ → x⁶ → x⁷
305    /// - x² (1 squaring)
306    /// - x³ = x² * x (1 multiplication)
307    /// - x⁶ = (x³)² (1 squaring)
308    /// - x⁷ = x⁶ * x (1 multiplication)
309    ///
310    /// Total: 2 squarings + 2 multiplications
311    #[inline(always)]
312    pub fn power7(a: [Felt; 3]) -> [Felt; 3] {
313        let a2 = square(a);
314        let a3 = mul(a2, a);
315        let a6 = square(a3);
316        mul(a6, a)
317    }
318}
319
320// PLONKY3 INTEGRATION
321// ================================================================================================
322
323/// Plonky3-compatible RPX permutation implementation.
324///
325/// This module provides a Plonky3-compatible interface to the RPX256 hash function,
326/// implementing the `Permutation` and `CryptographicPermutation` traits from Plonky3.
327///
328/// This allows RPX to be used with Plonky3's cryptographic infrastructure, including:
329/// - PaddingFreeSponge for hashing
330/// - TruncatedPermutation for compression
331/// - DuplexChallenger for Fiat-Shamir transforms
332use p3_challenger::DuplexChallenger;
333use p3_symmetric::{
334    CryptographicPermutation, PaddingFreeSponge, Permutation, TruncatedPermutation,
335};
336
337// RPX PERMUTATION FOR PLONKY3
338// ================================================================================================
339
340/// Plonky3-compatible RPX permutation.
341///
342/// This struct wraps the RPX256 permutation and implements Plonky3's `Permutation` and
343/// `CryptographicPermutation` traits, allowing RPX to be used within the Plonky3 ecosystem.
344///
345/// The permutation operates on a state of 12 field elements (STATE_WIDTH = 12), with:
346/// - Rate: 8 elements (positions 0-7)
347/// - Capacity: 4 elements (positions 8-11)
348/// - Digest output: 4 elements (positions 0-3)
349#[derive(Debug, Copy, Clone, Eq, PartialEq)]
350pub struct RpxPermutation256;
351
352impl RpxPermutation256 {
353    // CONSTANTS
354    // --------------------------------------------------------------------------------------------
355
356    /// Sponge state is set to 12 field elements or 768 bytes; 8 elements are reserved for rate and
357    /// the remaining 4 elements are reserved for capacity.
358    pub const STATE_WIDTH: usize = STATE_WIDTH;
359
360    /// The rate portion of the state is located in elements 0 through 7 (inclusive).
361    pub const RATE_RANGE: Range<usize> = Rpx256::RATE_RANGE;
362
363    /// The capacity portion of the state is located in elements 8, 9, 10, and 11.
364    pub const CAPACITY_RANGE: Range<usize> = Rpx256::CAPACITY_RANGE;
365
366    /// The output of the hash function can be read from state elements 0, 1, 2, and 3 (the first
367    /// word of the state).
368    pub const DIGEST_RANGE: Range<usize> = Rpx256::DIGEST_RANGE;
369
370    // RPX PERMUTATION
371    // --------------------------------------------------------------------------------------------
372
373    /// Applies RPX permutation to the provided state.
374    ///
375    /// This delegates to the RPX256 implementation.
376    #[inline(always)]
377    pub fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) {
378        Rpx256::apply_permutation(state);
379    }
380}
381
382// PLONKY3 TRAIT IMPLEMENTATIONS
383// ================================================================================================
384
385impl Permutation<[Felt; STATE_WIDTH]> for RpxPermutation256 {
386    fn permute_mut(&self, state: &mut [Felt; STATE_WIDTH]) {
387        Self::apply_permutation(state);
388    }
389}
390
391impl CryptographicPermutation<[Felt; STATE_WIDTH]> for RpxPermutation256 {}
392
393// TYPE ALIASES FOR PLONKY3 INTEGRATION
394// ================================================================================================
395
396/// RPX-based hasher using Plonky3's PaddingFreeSponge.
397///
398/// This provides a sponge-based hash function with:
399/// - WIDTH: 12 field elements (total state size)
400/// - RATE: 8 field elements (input/output rate)
401/// - OUT: 4 field elements (digest size)
402pub type RpxHasher = PaddingFreeSponge<RpxPermutation256, 12, 8, 4>;
403
404/// RPX-based compression function using Plonky3's TruncatedPermutation.
405///
406/// This provides a 2-to-1 compression function for Merkle tree construction with:
407/// - CHUNK: 2 (number of input chunks - i.e., 2 digests of 4 elements each = 8 elements)
408/// - N: 4 (output size in field elements)
409/// - WIDTH: 12 (total state size)
410///
411/// The compression function takes 8 field elements (2 digests) as input and produces
412/// 4 field elements (1 digest) as output.
413pub type RpxCompression = TruncatedPermutation<RpxPermutation256, 2, 4, 12>;
414
415/// RPX-based challenger using Plonky3's DuplexChallenger.
416///
417/// This provides a Fiat-Shamir transform implementation for interactive proof protocols,
418/// with:
419/// - F: Generic field type (typically the same as Felt)
420/// - WIDTH: 12 field elements (sponge state size)
421/// - RATE: 8 field elements (rate of absorption/squeezing)
422pub type RpxChallenger<F> = DuplexChallenger<F, RpxPermutation256, 12, 8>;