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>;