mina_poseidon/sponge.rs
1extern crate alloc;
2
3use crate::{
4 constants::{PlonkSpongeConstantsKimchi, SpongeConstants},
5 poseidon::{ArithmeticSponge, ArithmeticSpongeParams, Sponge},
6};
7use alloc::{vec, vec::Vec};
8use ark_ec::models::short_weierstrass::{Affine, SWCurveConfig};
9use ark_ff::{BigInteger, Field, One, PrimeField, Zero};
10
11/// Abstracts a sponge operating on a base field `Fq` of the curve
12/// `G`. The parameter `Fr` is modelling the scalar field of the
13/// curve.
14pub trait FqSponge<Fq: Field, G, Fr, const FULL_ROUNDS: usize> {
15 /// Creates a new sponge.
16 fn new(p: &'static ArithmeticSpongeParams<Fq, FULL_ROUNDS>) -> Self;
17
18 /// Absorbs a base field element. This operation is the most
19 /// straightforward and calls the underlying sponge directly.
20 fn absorb_fq(&mut self, x: &[Fq]);
21
22 /// Absorbs a base field point, that is a pair of `Fq` elements.
23 /// In the case of the point to infinity, the values `(0, 0)` are absorbed.
24 fn absorb_g(&mut self, g: &[G]);
25
26 /// Absorbs an element of the scalar field `Fr` --- it is done
27 /// by converting the element to the base field first.
28 fn absorb_fr(&mut self, x: &[Fr]);
29
30 /// Squeeze out a base field challenge. This operation is the most
31 /// direct and calls the underlying sponge.
32 fn challenge_fq(&mut self) -> Fq;
33
34 /// Squeeze out a challenge in the scalar field. Implemented by
35 /// squeezing out base points and then converting them to a scalar
36 /// field element using binary representation.
37 fn challenge(&mut self) -> Fr;
38
39 /// Returns a base field digest by squeezing the underlying sponge directly.
40 fn digest_fq(self) -> Fq;
41
42 /// Returns a scalar field digest using the binary representation technique.
43 fn digest(self) -> Fr;
44}
45
46/// Number of 64-bit limbs used to represent a scalar challenge.
47///
48/// With 2 limbs, challenges are 128 bits. This is sufficient because:
49///
50/// **Endomorphism decomposition**: The challenge is converted to an effective
51/// scalar k = a·λ + b where both a and b are derived from the 128-bit input.
52/// Since λ is a cube root of unity in a ~255-bit scalar field, a 128-bit
53/// challenge provides enough entropy for both components.
54pub const CHALLENGE_LENGTH_IN_LIMBS: usize = 2;
55
56const HIGH_ENTROPY_LIMBS: usize = 2;
57
58/// A challenge which is used as a scalar on a group element in the verifier.
59///
60/// This wraps a field element that will be converted to an "effective" scalar
61/// using the curve endomorphism for efficient scalar multiplication.
62///
63/// See [`ScalarChallenge::to_field`] for how the conversion works.
64#[derive(Clone, Debug)]
65pub struct ScalarChallenge<F>(F);
66
67impl<F> ScalarChallenge<F> {
68 /// Creates a [`ScalarChallenge`](Self) from a field element.
69 ///
70 /// # Deprecation
71 ///
72 /// This constructor will be deprecated in favor of [`Self::from_limbs`],
73 /// which enforces the 128-bit constraint at construction.
74 ///
75 /// The field element is assumed to contain at most 128 bits of data
76 /// (i.e., only the two lowest 64-bit limbs are set). This is the case
77 /// when the value comes from [`FqSponge::challenge`].
78 pub const fn new(challenge: F) -> Self {
79 Self(challenge)
80 }
81}
82
83/// Computes a primitive cube root of unity ξ in the field F.
84///
85/// For a prime field `F_p` where 3 divides p-1, this returns:
86///
87/// ξ = g^((p-1)/3)
88///
89/// where g is a generator of the multiplicative group `F_p*`.
90///
91/// # Properties
92///
93/// - ξ³ = g^(p-1) = 1 (by Fermat's Little Theorem)
94/// - ξ ≠ 1 (since (p-1)/3 is not a multiple of p-1)
95/// - The three cube roots of unity are: {1, ξ, ξ²}
96///
97/// # Usage
98///
99/// This is used in two contexts:
100///
101/// 1. **Base field (ξ)**: For the curve endomorphism φ(x,y) = (ξ·x, y)
102/// 2. **Scalar field (λ)**: As the scalar such that `φ(P) = [λ]P`
103///
104/// Both fields (Fp and Fq for Pasta curves) have cube roots of unity,
105/// and they correspond to each other via the endomorphism relationship.
106///
107/// # References
108///
109/// - Halo paper, Section 6.2: <https://eprint.iacr.org/2019/1021>
110pub fn endo_coefficient<F: PrimeField>() -> F {
111 let p_minus_1_over_3 = (F::zero() - F::one()) / F::from(3u64);
112
113 F::GENERATOR.pow(p_minus_1_over_3.into_bigint().as_ref())
114}
115
116fn get_bit(limbs_lsb: &[u64], i: u64) -> u64 {
117 let limb = i / 64;
118 let j = i % 64;
119 (limbs_lsb[limb as usize] >> j) & 1
120}
121
122impl<F: PrimeField> ScalarChallenge<F> {
123 /// Creates a [`ScalarChallenge`](Self) from exactly 128 bits (2 limbs).
124 ///
125 /// This is the preferred constructor as it enforces the 128-bit constraint
126 /// required by [`Self::to_field`].
127 ///
128 /// # Panics
129 ///
130 /// Panics if the 128-bit value cannot be represented as a field element
131 /// (unreachable for fields with modulus > 2^128).
132 #[must_use]
133 pub fn from_limbs(limbs: [u64; 2]) -> Self {
134 Self(F::from_bigint(pack(&limbs)).expect("128 bits always fits in field"))
135 }
136
137 /// Get the inner value
138 #[must_use]
139 pub const fn inner(&self) -> F {
140 self.0
141 }
142
143 /// Converts a scalar challenge to an "effective" scalar using endomorphism
144 /// decomposition.
145 ///
146 /// # Background
147 ///
148 /// For curves with an endomorphism `φ(P) = [λ]P`, we can represent any scalar
149 /// `k` as:
150 ///
151 /// k = a·λ + b
152 ///
153 /// This allows efficient scalar multiplication because:
154 ///
155 /// `[k]P = [a·λ + b]P = [a]·φ(P) + [b]·P`
156 ///
157 /// Since φ(P) = (ξ·x, y) is essentially free (one field multiplication),
158 /// we reduce the scalar multiplication cost by processing two scalar
159 /// multiplications of half the size instead of one full-size multiplication.
160 ///
161 /// # Algorithm
162 ///
163 /// Starting with a = b = 2, the challenge bits are processed in pairs
164 /// (r_{2i}, r_{2i+1}) from MSB to LSB. For each pair:
165 ///
166 /// 1. Double both a and b
167 /// 2. Add ±1 to either a or b based on the bit pair:
168 ///
169 /// | r_{2i} | r_{2i+1} | Action |
170 /// |--------|----------|---------|
171 /// | 0 | 0 | b += -1 |
172 /// | 1 | 0 | b += +1 |
173 /// | 0 | 1 | a += -1 |
174 /// | 1 | 1 | a += +1 |
175 ///
176 /// The result is: a·λ + b
177 ///
178 /// # Parameters
179 ///
180 /// - `length_in_bits`: Number of bits to process from the challenge
181 /// - `endo_coeff`: The scalar λ such that `φ(P) = [λ]P`
182 ///
183 /// # Returns
184 ///
185 /// The effective scalar k = a·λ + b
186 ///
187 /// # References
188 ///
189 /// - Halo paper, Section 6.2: <https://eprint.iacr.org/2019/1021>
190 pub fn to_field_with_length(&self, length_in_bits: usize, endo_coeff: &F) -> F {
191 let rep = self.0.into_bigint();
192 let r = rep.as_ref();
193
194 let mut a: F = 2_u64.into();
195 let mut b: F = 2_u64.into();
196
197 let one = F::one();
198 let neg_one = -one;
199
200 for i in (0..(length_in_bits as u64 / 2)).rev() {
201 a.double_in_place();
202 b.double_in_place();
203
204 let r_2i = get_bit(r, 2 * i);
205 let s = if r_2i == 0 { &neg_one } else { &one };
206
207 if get_bit(r, 2 * i + 1) == 0 {
208 b += s;
209 } else {
210 a += s;
211 }
212 }
213
214 a * endo_coeff + b
215 }
216
217 /// Converts a scalar challenge to an effective scalar.
218 ///
219 /// This is a convenience wrapper around [`Self::to_field_with_length`]
220 /// using the default challenge length (128 bits).
221 ///
222 /// See [`Self::to_field_with_length`] for details on the algorithm.
223 pub fn to_field(&self, endo_coeff: &F) -> F {
224 let length_in_bits = 64 * CHALLENGE_LENGTH_IN_LIMBS;
225 self.to_field_with_length(length_in_bits, endo_coeff)
226 }
227}
228
229#[derive(Clone)]
230pub struct DefaultFqSponge<P: SWCurveConfig, SC: SpongeConstants, const FULL_ROUNDS: usize> {
231 pub sponge: ArithmeticSponge<P::BaseField, SC, FULL_ROUNDS>,
232 pub last_squeezed: Vec<u64>,
233}
234
235pub struct DefaultFrSponge<Fr: Field, SC: SpongeConstants, const FULL_ROUNDS: usize> {
236 pub sponge: ArithmeticSponge<Fr, SC, FULL_ROUNDS>,
237 pub last_squeezed: Vec<u64>,
238}
239
240impl<const FULL_ROUNDS: usize, Fr> From<&'static ArithmeticSpongeParams<Fr, FULL_ROUNDS>>
241 for DefaultFrSponge<Fr, PlonkSpongeConstantsKimchi, FULL_ROUNDS>
242where
243 Fr: PrimeField,
244{
245 fn from(p: &'static ArithmeticSpongeParams<Fr, FULL_ROUNDS>) -> Self {
246 Self {
247 sponge: ArithmeticSponge::new(p),
248 last_squeezed: vec![],
249 }
250 }
251}
252
253fn pack<B: BigInteger>(limbs_lsb: &[u64]) -> B {
254 let mut res: B = 0u64.into();
255 for &x in limbs_lsb.iter().rev() {
256 res <<= 64;
257 res.add_with_carry(&x.into());
258 }
259 res
260}
261
262impl<Fr: PrimeField, SC: SpongeConstants, const FULL_ROUNDS: usize>
263 DefaultFrSponge<Fr, SC, FULL_ROUNDS>
264{
265 pub fn squeeze(&mut self, num_limbs: usize) -> Fr {
266 if self.last_squeezed.len() >= num_limbs {
267 let last_squeezed = self.last_squeezed.clone();
268 let (limbs, remaining) = last_squeezed.split_at(num_limbs);
269 self.last_squeezed = remaining.to_vec();
270 Fr::from(pack::<Fr::BigInt>(limbs))
271 } else {
272 let x = self.sponge.squeeze().into_bigint();
273 self.last_squeezed
274 .extend(&x.as_ref()[0..HIGH_ENTROPY_LIMBS]);
275 self.squeeze(num_limbs)
276 }
277 }
278}
279
280impl<P: SWCurveConfig, SC: SpongeConstants, const FULL_ROUNDS: usize>
281 DefaultFqSponge<P, SC, FULL_ROUNDS>
282where
283 P::BaseField: PrimeField,
284 <P::BaseField as PrimeField>::BigInt: Into<<P::ScalarField as PrimeField>::BigInt>,
285{
286 pub fn squeeze_limbs(&mut self, num_limbs: usize) -> Vec<u64> {
287 if self.last_squeezed.len() >= num_limbs {
288 let last_squeezed = self.last_squeezed.clone();
289 let (limbs, remaining) = last_squeezed.split_at(num_limbs);
290 self.last_squeezed = remaining.to_vec();
291 limbs.to_vec()
292 } else {
293 let x = self.sponge.squeeze().into_bigint();
294 self.last_squeezed
295 .extend(&x.as_ref()[0..HIGH_ENTROPY_LIMBS]);
296 self.squeeze_limbs(num_limbs)
297 }
298 }
299
300 pub fn squeeze_field(&mut self) -> P::BaseField {
301 self.last_squeezed = vec![];
302 self.sponge.squeeze()
303 }
304
305 /// Squeeze out a scalar field element from the sponge.
306 ///
307 /// # Panics
308 ///
309 /// Panics if the packed limbs cannot be converted to a valid scalar
310 /// field element.
311 pub fn squeeze(&mut self, num_limbs: usize) -> P::ScalarField {
312 P::ScalarField::from_bigint(pack(&self.squeeze_limbs(num_limbs)))
313 .expect("internal representation was not a valid field element")
314 }
315}
316
317impl<P: SWCurveConfig, SC: SpongeConstants, const FULL_ROUNDS: usize>
318 FqSponge<P::BaseField, Affine<P>, P::ScalarField, FULL_ROUNDS>
319 for DefaultFqSponge<P, SC, FULL_ROUNDS>
320where
321 P::BaseField: PrimeField,
322 <P::BaseField as PrimeField>::BigInt: Into<<P::ScalarField as PrimeField>::BigInt>,
323{
324 fn new(params: &'static ArithmeticSpongeParams<P::BaseField, FULL_ROUNDS>) -> Self {
325 let sponge = ArithmeticSponge::new(params);
326 Self {
327 sponge,
328 last_squeezed: vec![],
329 }
330 }
331
332 fn absorb_g(&mut self, g: &[Affine<P>]) {
333 self.last_squeezed = vec![];
334 for pt in g {
335 if pt.infinity {
336 // absorb a fake point (0, 0)
337 let zero = P::BaseField::zero();
338 self.sponge.absorb(&[zero]);
339 self.sponge.absorb(&[zero]);
340 } else {
341 self.sponge.absorb(&[pt.x]);
342 self.sponge.absorb(&[pt.y]);
343 }
344 }
345 }
346
347 fn absorb_fq(&mut self, x: &[P::BaseField]) {
348 self.last_squeezed = vec![];
349
350 for fe in x {
351 self.sponge.absorb(&[*fe]);
352 }
353 }
354
355 fn absorb_fr(&mut self, x: &[P::ScalarField]) {
356 self.last_squeezed = vec![];
357
358 for elem in x {
359 let bits = elem.into_bigint().to_bits_le();
360
361 // absorb
362 if <P::ScalarField as PrimeField>::MODULUS
363 < <P::BaseField as PrimeField>::MODULUS.into()
364 {
365 let fe = P::BaseField::from_bigint(
366 <P::BaseField as PrimeField>::BigInt::from_bits_le(&bits),
367 )
368 .expect("padding code has a bug");
369 self.sponge.absorb(&[fe]);
370 } else {
371 let low_bit = if bits[0] {
372 P::BaseField::one()
373 } else {
374 P::BaseField::zero()
375 };
376
377 let high_bits = P::BaseField::from_bigint(
378 <P::BaseField as PrimeField>::BigInt::from_bits_le(&bits[1..bits.len()]),
379 )
380 .expect("padding code has a bug");
381
382 self.sponge.absorb(&[high_bits]);
383 self.sponge.absorb(&[low_bit]);
384 }
385 }
386 }
387
388 fn digest(mut self) -> P::ScalarField {
389 let x: <P::BaseField as PrimeField>::BigInt = self.squeeze_field().into_bigint();
390 // Returns zero for values that are too large.
391 // This means that there is a bias for the value zero (in one of the curve).
392 // An attacker could try to target that seed, in order to predict the challenges u and v produced by the Fr-Sponge.
393 // This would allow the attacker to mess with the result of the aggregated evaluation proof.
394 // Previously the attacker's odds were 1/q, now it's (q-p)/q.
395 // Since log2(q-p) ~ 86 and log2(q) ~ 254 the odds of a successful attack are negligible.
396 P::ScalarField::from_bigint(x.into()).unwrap_or_else(P::ScalarField::zero)
397 }
398
399 fn digest_fq(mut self) -> P::BaseField {
400 self.squeeze_field()
401 }
402
403 fn challenge(&mut self) -> P::ScalarField {
404 self.squeeze(CHALLENGE_LENGTH_IN_LIMBS)
405 }
406
407 fn challenge_fq(&mut self) -> P::BaseField {
408 self.squeeze_field()
409 }
410}
411
412//
413// OCaml types
414//
415
416#[cfg(feature = "ocaml_types")]
417#[allow(non_local_definitions)]
418pub mod caml {
419 // The ocaml_gen::Struct derive macro requires items from parent scope
420 // that cannot be enumerated explicitly.
421 #[allow(clippy::wildcard_imports)]
422 use super::*;
423
424 extern crate alloc;
425 use alloc::{
426 format,
427 string::{String, ToString},
428 };
429
430 //
431 // ScalarChallenge<F> <-> CamlScalarChallenge<CamlF>
432 //
433
434 #[derive(Debug, Clone, ocaml::IntoValue, ocaml::FromValue, ocaml_gen::Struct)]
435 pub struct CamlScalarChallenge<CamlF>(pub CamlF);
436
437 impl<F, CamlF> From<ScalarChallenge<F>> for CamlScalarChallenge<CamlF>
438 where
439 CamlF: From<F>,
440 {
441 fn from(sc: ScalarChallenge<F>) -> Self {
442 Self(sc.0.into())
443 }
444 }
445
446 impl<F, CamlF> From<CamlScalarChallenge<CamlF>> for ScalarChallenge<F>
447 where
448 CamlF: Into<F>,
449 {
450 fn from(caml_sc: CamlScalarChallenge<CamlF>) -> Self {
451 Self(caml_sc.0.into())
452 }
453 }
454}