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