Skip to main content

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

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