Skip to main content

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}