Skip to main content

ark_ec/hashing/curve_maps/
swu.rs

1use crate::models::short_weierstrass::SWCurveConfig;
2use ark_ff::{Field, One, Zero};
3use core::marker::PhantomData;
4
5use crate::{
6    hashing::{curve_maps::parity, map_to_curve_hasher::MapToCurve, HashToCurveError},
7    models::short_weierstrass::{Affine, Projective},
8};
9
10/// Trait defining the necessary parameters for the SWU hash-to-curve method
11/// for the curves of Weierstrass form of:
12/// y^2 = x^3 + a*x + b where ab != 0. From [\[WB2019\]]
13///
14/// - [\[WB2019\]] <https://eprint.iacr.org/2019/403>
15pub trait SWUConfig: SWCurveConfig {
16    /// An element of the base field that is not a square root see \[WB2019, Section 4\].
17    /// It is also convenient to have $g(b/ZETA * a)$ to be square. In general
18    /// we use a `ZETA` with low absolute value coefficients when they are
19    /// represented as integers.
20    const ZETA: Self::BaseField;
21}
22
23/// Represents the SWU hash-to-curve map defined by `P`.
24pub struct SWUMap<P: SWUConfig>(PhantomData<fn() -> P>);
25
26impl<P: SWUConfig> MapToCurve<Projective<P>> for SWUMap<P> {
27    /// Checks if `P` represents a valid map.
28    fn check_parameters() -> Result<(), HashToCurveError> {
29        // Verifying that ZETA is a non-square
30        debug_assert!(
31            P::ZETA.legendre().is_qnr(),
32            "ZETA should be a quadratic non-residue for the SWU map"
33        );
34
35        // Verifying the prerequisite for applicability  of SWU map
36        debug_assert!(!P::COEFF_A.is_zero() && !P::COEFF_B.is_zero(),
37		      "Simplified SWU requires a * b != 0 in the short Weierstrass form of y^2 = x^3 + a*x + b ");
38
39        Ok(())
40    }
41
42    /// Map an arbitrary base field element to a curve point.
43    /// Based on
44    /// <https://github.com/zcash/pasta_curves/blob/main/src/hashtocurve.rs>.
45    fn map_to_curve(element: P::BaseField) -> Result<Affine<P>, HashToCurveError> {
46        // 1. tv1 = inv0(Z^2 * u^4 + Z * u^2)
47        // 2. x1 = (-B / A) * (1 + tv1)
48        // 3. If tv1 == 0, set x1 = B / (Z * A)
49        // 4. gx1 = x1^3 + A * x1 + B
50        //
51        // We use the "Avoiding inversions" optimization in [WB2019, section 4.2]
52        // (not to be confused with section 4.3):
53        //
54        //   here       [WB2019]
55        //   -------    ---------------------------------
56        //   Z          ξ
57        //   u          t
58        //   Z * u^2    ξ * t^2 (called u, confusingly)
59        //   x1         X_0(t)
60        //   x2         X_1(t)
61        //   gx1        g(X_0(t))
62        //   gx2        g(X_1(t))
63        //
64        // Using the "here" names:
65        //    x1 = num_x1/div      = [B*(Z^2 * u^4 + Z * u^2 + 1)] / [-A*(Z^2 * u^4 + Z * u^2]
66        //   gx1 = num_gx1/div_gx1 = [num_x1^3 + A * num_x1 * div^2 + B * div^3] / div^3
67        let a = P::COEFF_A;
68        let b = P::COEFF_B;
69
70        let zeta_u2 = P::ZETA * element.square();
71        let ta = zeta_u2.square() + zeta_u2;
72        let num_x1 = b * (ta + <P::BaseField as One>::one());
73        let div = a * if ta.is_zero() { P::ZETA } else { -ta };
74
75        let num2_x1 = num_x1.square();
76        let div2 = div.square();
77        let div3 = div2 * div;
78        let num_gx1 = (num2_x1 + a * div2) * num_x1 + b * div3;
79
80        // 5. x2 = Z * u^2 * x1
81        let num_x2 = zeta_u2 * num_x1; // same div
82
83        // 6. gx2 = x2^3 + A * x2 + B  [optimized out; see below]
84        // 7. If is_square(gx1), set x = x1 and y = sqrt(gx1)
85        // 8. Else set x = x2 and y = sqrt(gx2)
86        let gx1_square;
87        let gx1;
88
89        debug_assert!(
90            !div3.is_zero(),
91            "we have checked that neither a or ZETA are zero. Q.E.D."
92        );
93        let y1: P::BaseField = {
94            gx1 = num_gx1 / div3;
95            if gx1.legendre().is_qr() {
96                gx1_square = true;
97                gx1.sqrt()
98                    .expect("We have checked that gx1 is a quadratic residue. Q.E.D")
99            } else {
100                let zeta_gx1 = P::ZETA * gx1;
101                gx1_square = false;
102                zeta_gx1.sqrt().expect(
103                    "ZETA * gx1 is a quadratic residue because legard is multiplicative. Q.E.D",
104                )
105            }
106        };
107
108        // This optimization also comes from a generalization of [WB2019, section 4.2].
109        //
110        // We use the specialization with h = Z = ZETA (a fixed quadratic non-residue).
111        // Since gx2 = g(Z * u^2 * x1) = Z^3 * u^6 * gx1, when gx1 is not square we take
112        // y1 such that y1^2 = Z * gx1 (i.e., y1 = sqrt(Z * gx1)), and then set
113        // y2 = Z * u^3 * y1. This gives y2^2 = (Z * u^3)^2 * (Z * gx1) = Z^3 * u^6 * gx1 = gx2,
114        // so we avoid computing gx2 explicitly.
115
116        let y2 = zeta_u2 * element * y1;
117        let num_x = if gx1_square { num_x1 } else { num_x2 };
118        let y = if gx1_square { y1 } else { y2 };
119
120        let x_affine = num_x / div;
121        let y_affine = if parity(&y) == parity(&element) {
122            y
123        } else {
124            -y
125        };
126        let point_on_curve = Affine::new_unchecked(x_affine, y_affine);
127        debug_assert!(
128            point_on_curve.is_on_curve(),
129            "swu mapped to a point off the curve"
130        );
131        Ok(point_on_curve)
132    }
133}
134
135#[cfg(test)]
136mod test {
137    #[cfg(all(
138        target_has_atomic = "8",
139        target_has_atomic = "16",
140        target_has_atomic = "32",
141        target_has_atomic = "64",
142        target_has_atomic = "ptr"
143    ))]
144    type DefaultHasher = ahash::AHasher;
145
146    #[cfg(not(all(
147        target_has_atomic = "8",
148        target_has_atomic = "16",
149        target_has_atomic = "32",
150        target_has_atomic = "64",
151        target_has_atomic = "ptr"
152    )))]
153    type DefaultHasher = fnv::FnvHasher;
154
155    use crate::{
156        hashing::{map_to_curve_hasher::MapToCurveBasedHasher, HashToCurve},
157        CurveConfig,
158    };
159    use ark_ff::field_hashers::DefaultFieldHasher;
160    use ark_std::vec::*;
161
162    use super::*;
163    use ark_ff::{fields::Fp64, MontBackend, MontFp};
164    use hashbrown::HashMap;
165    use sha2::Sha256;
166
167    #[derive(ark_ff::MontConfig)]
168    #[modulus = "127"]
169    #[generator = "6"]
170    pub(crate) struct F127Config;
171    pub(crate) type F127 = Fp64<MontBackend<F127Config, 1>>;
172
173    const F127_ONE: F127 = MontFp!("1");
174
175    struct TestSWUMapToCurveConfig;
176
177    impl CurveConfig for TestSWUMapToCurveConfig {
178        const COFACTOR: &[u64] = &[1];
179
180        const COFACTOR_INV: F127 = F127_ONE;
181
182        type BaseField = F127;
183        type ScalarField = F127;
184    }
185
186    /// just because not defining another field
187    ///
188    /// from itertools import product
189    /// p = 127
190    /// FF = GF(p)
191    /// for a,b in product(range(0,p), range(0,p)):
192    ///     try:
193    ///         E = EllipticCurve([FF(a),FF(b)])
194    ///         if E.order() == p:
195    ///             print(E)
196    ///     except:
197    ///         pass
198    ///
199    /// y^2 = x^3 + x + 63
200    impl SWCurveConfig for TestSWUMapToCurveConfig {
201        /// COEFF_A = 1
202        const COEFF_A: F127 = F127_ONE;
203
204        /// COEFF_B = 63
205        const COEFF_B: F127 = MontFp!("63");
206
207        /// AFFINE_GENERATOR_COEFFS = (G1_GENERATOR_X, G1_GENERATOR_Y)
208        const GENERATOR: Affine<Self> = Affine::new_unchecked(MontFp!("62"), MontFp!("70"));
209
210        /// We use `bool` because the point (0, 0) could be on the curve.
211        type ZeroFlag = bool;
212    }
213
214    impl SWUConfig for TestSWUMapToCurveConfig {
215        const ZETA: F127 = MontFp!("-1");
216    }
217
218    /// test that MontFp make a none zero element out of 1
219    #[test]
220    fn test_field_element_construction() {
221        let a1 = F127::from(1);
222        let a2 = F127::from(2);
223        let a3 = F127::from(125);
224
225        assert!(F127::from(0) == a2 + a3);
226        assert!(F127::from(0) == a2 * a1 + a3);
227    }
228
229    #[test]
230    fn test_field_division() {
231        let num = F127::from(0x3d);
232        let den = F127::from(0x7b);
233        let num_on_den = F127::from(0x50);
234
235        assert!(num / den == num_on_den);
236    }
237
238    /// The point of the test is to get a simple SWU compatible curve and make
239    /// simple hash
240    #[test]
241    fn hash_arbitrary_string_to_curve_swu() {
242        let test_swu_to_curve_hasher = MapToCurveBasedHasher::<
243            Projective<TestSWUMapToCurveConfig>,
244            DefaultFieldHasher<Sha256, 128>,
245            SWUMap<TestSWUMapToCurveConfig>,
246        >::new(&[1])
247        .unwrap();
248
249        let hash_result = test_swu_to_curve_hasher.hash(b"if you stick a Babel fish in your ear you can instantly understand anything said to you in any form of language.").expect("fail to hash the string to curve");
250
251        assert!(
252            hash_result.is_on_curve(),
253            "hash results into a point off the curve"
254        );
255    }
256
257    /// Use a simple SWU compatible curve and map the whole field to it. We observe
258    /// the map behaviour. Specifically, the map should be non-constant, all
259    /// elements should be mapped to curve successfully. everything can be mapped
260    #[test]
261    fn map_field_to_curve_swu() {
262        SWUMap::<TestSWUMapToCurveConfig>::check_parameters().unwrap();
263
264        let mut map_range: Vec<Affine<TestSWUMapToCurveConfig>> = Vec::with_capacity(128);
265        for current_field_element in 0..127 {
266            let element = F127::from(current_field_element as u64);
267            map_range.push(SWUMap::map_to_curve(element).unwrap());
268        }
269
270        let mut counts =
271            HashMap::with_hasher(core::hash::BuildHasherDefault::<DefaultHasher>::default());
272
273        let mode = map_range
274            .iter()
275            .copied()
276            .max_by_key(|&n| {
277                let count = counts.entry(n).or_insert(0);
278                *count += 1;
279                *count
280            })
281            .unwrap();
282
283        assert!(
284            *counts.get(&mode).unwrap() != 127,
285            "a constant hash function is not good."
286        );
287    }
288}