From fa3afc29bbf499f4a8e66af69a5da4680e074c4d Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Wed, 13 Jan 2021 13:24:47 +0000 Subject: [PATCH] Add an implementation of simplified SWU hash-to-curve. Signed-off-by: Daira Hopwood --- Cargo.toml | 6 + benches/hashtocurve.rs | 47 +++++ src/arithmetic.rs | 2 + src/arithmetic/curves.rs | 26 ++- src/arithmetic/fields.rs | 2 +- src/arithmetic/hashtocurve.rs | 330 ++++++++++++++++++++++++++++++++++ src/pasta/curves.rs | 296 ++++++++++++++++++++++-------- src/pasta/fields/fp.rs | 2 +- src/pasta/fields/fq.rs | 2 +- src/pasta/pallas.rs | 183 ++++++++++++++++++- src/pasta/vesta.rs | 143 ++++++++++++++- 11 files changed, 941 insertions(+), 98 deletions(-) create mode 100644 benches/hashtocurve.rs create mode 100644 src/arithmetic/hashtocurve.rs diff --git a/Cargo.toml b/Cargo.toml index 0ad9658..b45e69c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,10 @@ criterion = "0.3" name = "arithmetic" harness = false +[[bench]] +name = "hashtocurve" +harness = false + [[bench]] name = "plonk" harness = false @@ -43,8 +47,10 @@ metrics = "0.14.2" num_cpus = "1.13" rand = "0.8" blake2b_simd = "0.5" +sha3 = "0.9.1" lazy_static = "1.4.0" static_assertions = "1.1.0" +byteorder = "1.4.2" # Temporary workaround for https://github.com/myrrlyn/funty/issues/3 funty = "=1.1.0" diff --git a/benches/hashtocurve.rs b/benches/hashtocurve.rs new file mode 100644 index 0000000..0175b8c --- /dev/null +++ b/benches/hashtocurve.rs @@ -0,0 +1,47 @@ +//! Benchmarks for hashing to the Pasta curves. + +use criterion::{criterion_group, criterion_main, Criterion}; + +use halo2::arithmetic::{HashToCurve, Shake128}; +use halo2::pasta::{pallas, vesta}; + +fn criterion_benchmark(c: &mut Criterion) { + bench_hash_to_curve(c); + bench_encode_to_curve(c); + bench_map_to_curve(c); +} + +fn bench_hash_to_curve(c: &mut Criterion) { + let mut group = c.benchmark_group("hash-to-curve"); + + let hash_pallas = pallas::MAP.hash_to_curve("z.cash:test", Shake128::default()); + group.bench_function("Pallas", |b| b.iter(|| hash_pallas(b"benchmark"))); + + let hash_vesta = vesta::MAP.hash_to_curve("z.cash:test", Shake128::default()); + group.bench_function("Vesta", |b| b.iter(|| hash_vesta(b"benchmark"))); +} + +fn bench_encode_to_curve(c: &mut Criterion) { + let mut group = c.benchmark_group("encode-to-curve"); + + let encode_pallas = pallas::MAP.encode_to_curve("z.cash:test", Shake128::default()); + group.bench_function("Pallas", |b| b.iter(|| encode_pallas(b"benchmark"))); + + let encode_vesta = vesta::MAP.encode_to_curve("z.cash:test", Shake128::default()); + group.bench_function("Vesta", |b| b.iter(|| encode_vesta(b"benchmark"))); +} + +fn bench_map_to_curve(c: &mut Criterion) { + let mut group = c.benchmark_group("map-to-curve"); + + let pallas_input = &pallas::Base::one(); + group.bench_function("Pallas", |b| { + b.iter(|| pallas::MAP.map_to_curve(pallas_input)) + }); + + let vesta_input = &vesta::Base::one(); + group.bench_function("Vesta", |b| b.iter(|| vesta::MAP.map_to_curve(vesta_input))); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/arithmetic.rs b/src/arithmetic.rs index dcb4189..b6dc672 100644 --- a/src/arithmetic.rs +++ b/src/arithmetic.rs @@ -6,9 +6,11 @@ use ff::Field; mod curves; mod fields; +mod hashtocurve; pub use curves::*; pub use fields::*; +pub use hashtocurve::*; /// This represents an element of a group with basic operations that can be /// performed. This allows an FFT implementation (for example) to operate diff --git a/src/arithmetic/curves.rs b/src/arithmetic/curves.rs index 50bc2d9..e23151f 100644 --- a/src/arithmetic/curves.rs +++ b/src/arithmetic/curves.rs @@ -11,7 +11,7 @@ use super::{FieldExt, Group}; use std::io::{self, Read, Write}; /// This trait is a common interface for dealing with elements of an elliptic -/// curve group in the "projective" form, where that arithmetic is usually more +/// curve group in a "projective" form, where that arithmetic is usually more /// efficient. pub trait Curve: Sized @@ -60,7 +60,7 @@ pub trait Curve: /// Obtains the additive identity. fn zero() -> Self; - /// Obtains the base point of the curve. + /// Obtains the base point of the curve, if defined. fn one() -> Self; /// Doubles this element. @@ -76,6 +76,9 @@ pub trait Curve: /// Converts this element into its affine form. fn to_affine(&self) -> Self::Affine; + /// Return the Jacobian coordinates of this point. + fn jacobian_coordinates(&self) -> (Self::Base, Self::Base, Self::Base); + /// Returns whether or not this element is on the curve; should /// always be true unless an "unchecked" API was used. fn is_on_curve(&self) -> Choice; @@ -84,8 +87,15 @@ pub trait Curve: /// sizes of the slices are different. fn batch_to_affine(v: &[Self], target: &mut [Self::Affine]); - /// Returns the curve constant b + /// Returns the curve constant a. + fn a() -> Self::Base; + + /// Returns the curve constant b. fn b() -> Self::Base; + + /// Obtains a point given Jacobian coordinates $X : Y : Z$, failing + /// if the coordinates are not on the curve. + fn new_jacobian(x: Self::Base, y: Self::Base, z: Self::Base) -> CtOption; } /// This trait is the affine counterpart to `Curve` and is used for @@ -128,10 +138,13 @@ pub trait CurveAffine: /// random string. const BLAKE2B_PERSONALIZATION: &'static [u8; 16]; + /// CURVE_ID used for hash-to-curve. + const CURVE_ID: &'static str; + /// Obtains the additive identity. fn zero() -> Self; - /// Obtains the base point of the curve. + /// Obtains the base point of the curve, if defined. fn one() -> Self; /// Returns whether or not this element is the identity. @@ -182,6 +195,9 @@ pub trait CurveAffine: /// element. fn to_bytes_wide(&self) -> [u8; 64]; - /// Returns the curve constant $b$ + /// Returns the curve constant $a$. + fn a() -> Self::Base; + + /// Returns the curve constant $b$. fn b() -> Self::Base; } diff --git a/src/arithmetic/fields.rs b/src/arithmetic/fields.rs index 7c8a788..59bd863 100644 --- a/src/arithmetic/fields.rs +++ b/src/arithmetic/fields.rs @@ -315,7 +315,7 @@ impl SqrtTables { (is_square, res) } - /// Common part of sqrt_ratio and sqrt_alt: return res given v = u^((T-1)/2) and uv = u * v. + /// Common part of sqrt_ratio and sqrt_alt: return their result given v = u^((T-1)/2) and uv = u * v. fn sqrt_common(&self, uv: &F, v: &F) -> F { let sqr = |x: F, i: u32| (0..i).fold(x, |x, _| x.square()); let inv = |x: F| self.inv[self.hasher.hash(&x)] as usize; diff --git a/src/arithmetic/hashtocurve.rs b/src/arithmetic/hashtocurve.rs new file mode 100644 index 0000000..4824a03 --- /dev/null +++ b/src/arithmetic/hashtocurve.rs @@ -0,0 +1,330 @@ +//! This module implements "simplified SWU" hashing to short Weierstrass curves +//! with a = 0. + +use byteorder::{BigEndian, WriteBytesExt}; +use core::fmt::Debug; +use core::marker::PhantomData; +use subtle::ConstantTimeEq; + +use super::{Curve, CurveAffine, FieldExt}; + +/// A method of hashing to an elliptic curve. +/// (If no isogeny is required, then C and I should be the same.) +/// +/// This is intended to conform to the work-in-progress Internet Draft +/// [IRTF-CFRG-Hash-to-Curve](https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-10.html). +pub trait HashToCurve, C: CurveAffine> { + /// The MAP_ID of this method as specified in + /// . + fn map_id(&self) -> &str; + + /// A non-uniform map from a field element to the isogenous curve. + fn map_to_curve(&self, u: &C::Base) -> I::Projective; + + /// The isogeny map from curve I to curve C. + /// (If no isogeny is required, this should be the identity function.) + fn iso_map(&self, p: &I::Projective) -> C::Projective; + + /// The random oracle map. + fn field_elements_to_curve(&self, u0: &C::Base, u1: &C::Base) -> C::Projective; + + /// The full hash from an input message to a curve point. + /// + /// `domain_prefix` should identify the application protocol, usage + /// within that protocol, and version, e.g. "z.cash:Orchard-V1". + /// Other fields required to conform to [IRTF-CFRG-Hash-to-Curve] + /// will be added automatically. There may be a length limitation on + /// `domain_prefix`. + /// + /// For example, the resulting full domain separation tag for the + /// Pallas curve using `Shake128` and the simplified SWU map might be + /// b"z.cash:Orchard-V1-pallas_XOF:SHAKE128_SSWU_RO_". + fn hash_to_curve( + &self, + domain_prefix: &str, + hasher: impl MessageHasher + 'static, + ) -> Box C::Projective + '_> { + let domain_separation_tag = format!( + "{}-{}_{}_{}_RO_", + domain_prefix, + C::CURVE_ID, + hasher.hash_name(), + self.map_id() + ); + + Box::new(move |message| { + let us = hasher.hash_to_field(message, domain_separation_tag.as_bytes(), 2); + self.field_elements_to_curve(&us[0], &us[1]) + }) + } + + /// A non-uniform hash from an input message to a curve point. + /// This is *not* suitable for applications requiring a random oracle. + /// Use `hash_to_curve` instead unless you are really sure that a + /// non-uniform map is sufficient. + /// + /// `domain_prefix` is as described for `hash_to_curve`. + /// + /// For example, the resulting full domain separation tag for the + /// Pallas curve using `Shake128` and the simplified SWU map might be + /// b"z.cash:Orchard-V1-pallas_XOF:SHAKE128_SSWU_NU_". + fn encode_to_curve( + &self, + domain_prefix: &str, + hasher: impl MessageHasher + 'static, + ) -> Box C::Projective + '_> { + let domain_separation_tag = format!( + "{}-{}_{}_{}_NU_", + domain_prefix, + C::CURVE_ID, + hasher.hash_name(), + self.map_id() + ); + + Box::new(move |message| { + let us = hasher.hash_to_field(message, domain_separation_tag.as_bytes(), 1); + let r = self.map_to_curve(&us[0]); + self.iso_map(&r) + }) + } +} + +/// Method of hashing a message and domain_separation_tag to field elements. +pub trait MessageHasher { + /// The HASH_NAME of this message hasher as specified in + /// . + fn hash_name(&self) -> &str; + + /// Hash the given message and domain separation tag to give `count` + /// field elements. + fn hash_to_field(&self, message: &[u8], domain_separation_tag: &[u8], count: usize) -> Vec; +} + +/// A MessageHasher for SHAKE128 +/// [FIPS202](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf). +/// It does not support domain separation tags longer than 128 bytes. +#[derive(Debug, Default)] +pub struct Shake128 { + marker: PhantomData, +} + +impl MessageHasher for Shake128 { + fn hash_name(&self) -> &str { + "XOF:SHAKE128" + } + + fn hash_to_field(&self, message: &[u8], domain_separation_tag: &[u8], count: usize) -> Vec { + use sha3::digest::{ExtendableOutput, Update}; + assert!(domain_separation_tag.len() < 256); + + // Assume that the field size is 32 bytes and k is 256, where k is defined in + // . + const CHUNKLEN: usize = 64; + + let outlen = count * CHUNKLEN; + let mut outlen_enc = vec![]; + outlen_enc.write_u32::(outlen as u32).unwrap(); + + let mut xof = sha3::Shake128::default(); + xof.update(message); + xof.update(outlen_enc); + xof.update([domain_separation_tag.len() as u8]); + xof.update(domain_separation_tag); + + xof.finalize_boxed(outlen) + .chunks(CHUNKLEN) + .map(|big| { + let mut little = [0u8; CHUNKLEN]; + little.copy_from_slice(big); + little.reverse(); + F::from_bytes_wide(&little) + }) + .collect() + } +} + +/// A MessageHasher for BLAKE2b. +#[derive(Debug, Default)] +pub struct Blake2bXof { + marker: PhantomData, +} + +impl MessageHasher for Blake2bXof { + fn hash_name(&self) -> &str { + "XOF:BLAKE2b" + } + + #[allow(unused_variables)] + fn hash_to_field(&self, message: &[u8], domain_separation_tag: &[u8], count: usize) -> Vec { + todo!() + } +} + +/// The simplified SWU hash-to-curve method, using an isogenous curve +/// y^2 = x^3 + a*x + b. This currently only supports prime-order curves. +#[derive(Debug)] +pub struct SimplifiedSWUWithDegree3Isogeny< + F: FieldExt, + I: CurveAffine, + C: CurveAffine, +> { + /// `Z` parameter (ξ in [WB2019]). + pub z: F, + + /// Precomputed -b/a for the isogenous curve. + pub minus_b_over_a: F, + + /// Precomputed b/Za for the isogenous curve. + pub b_over_za: F, + + /// Precomputed sqrt(Z / ROOT_OF_UNITY). + pub theta: F, + + /// Constants for the isogeny. + pub isogeny_constants: [F; 13], + + marker_curve: PhantomData, + marker_iso: PhantomData, +} + +impl, C: CurveAffine> + SimplifiedSWUWithDegree3Isogeny +{ + /// Create a SimplifiedSWUWithDegree3Isogeny method for the given parameters. + /// + /// # Panics + /// Panics if z is square. + pub fn new(z: &F, isogeny_constants: &[F; 13]) -> Self { + let a = I::a(); + let b = I::b(); + + SimplifiedSWUWithDegree3Isogeny { + z: *z, + minus_b_over_a: (-b) * &(a.invert().unwrap()), + b_over_za: b * &((*z * a).invert().unwrap()), + theta: (F::ROOT_OF_UNITY.invert().unwrap() * z).sqrt().unwrap(), + isogeny_constants: *isogeny_constants, + marker_curve: PhantomData, + marker_iso: PhantomData, + } + } +} + +impl, C: CurveAffine> HashToCurve + for SimplifiedSWUWithDegree3Isogeny +{ + fn map_id(&self) -> &str { + "SSWU" + } + + fn map_to_curve(&self, u: &F) -> I::Projective { + // 1. tv1 = inv0(Z^2 * u^4 + Z * u^2) + // 2. x1 = (-B / A) * (1 + tv1) + // 3. If tv1 == 0, set x1 = B / (Z * A) + // 4. gx1 = x1^3 + A * x1 + B + // + // We use the "Avoiding inversions" optimization in [WB2019, section 4.2] + // (not to be confused with section 4.3): + // + // here [WB2019] + // ------- --------------------------------- + // Z ξ + // u t + // Z * u^2 ξ * t^2 (called u, confusingly) + // x1 X_0(t) + // x2 X_1(t) + // gx1 g(X_0(t)) + // gx2 g(X_1(t)) + // + // Using the "here" names: + // x1 = num_x1/div = [B*(Z^2 * u^4 + Z * u^2 + 1)] / [-A*(Z^2 * u^4 + Z * u^2] + // gx1 = num_gx1/div_gx1 = [num_x1^3 + A * num_x1 * div^2 + B * div^3] / div^3 + + let a = I::a(); + let b = I::b(); + let z_u2 = self.z * u.square(); + let ta = z_u2.square() + z_u2; + let num_x1 = b * (ta + F::one()); + let div = -a * ta; + let num2_x1 = num_x1.square(); + let div2 = div.square(); + let div3 = div2 * div; + let ta_is_zero = ta.ct_is_zero(); + let num_gx1 = F::conditional_select( + &((num2_x1 + a * div2) * num_x1 + b * div3), + &self.b_over_za, + ta_is_zero, + ); + let div_gx1 = F::conditional_select(&div3, &F::one(), ta_is_zero); + + // 5. x2 = Z * u^2 * x1 + let num_x2 = z_u2 * num_x1; // same div + + // 6. gx2 = x2^3 + A * x2 + B [optimized out; see below] + // 7. If is_square(gx1), set x = x1 and y = sqrt(gx1) + // 8. Else set x = x2 and y = sqrt(gx2) + let (gx1_square, y1) = F::sqrt_ratio(&num_gx1, &div_gx1); + + // This magic also comes from a generalization of [WB2019, section 4.2]. + // + // The Sarkar square root algorithm with input s gives us a square root of + // ROOT_OF_UNITY * s for free when s is not square, where h is a fixed nonsquare. + // We know that Z / ROOT_OF_UNITY is a square since both Z and ROOT_OF_UNITY are + // nonsquares. Precompute theta as a square root of Z / ROOT_OF_UNITY. + // + // We have gx2 = g(Z * u^2 * x1) = Z^3 * u^6 * gx1 + // = (Z * u^3)^2 * (Z/h * h * gx1) + // = (Z * theta * u^3)^2 * (h * gx1) + // + // When gx1 is not square, y1 is a square root of h * gx1, and so Z * theta * u^3 * y1 + // is a square root of gx2. Note that we don't actually need to compute gx2. + + let y2 = self.theta * z_u2 * u * y1; + let num_x = F::conditional_select(&num_x2, &num_x1, gx1_square); + let y = F::conditional_select(&y2, &y1, gx1_square); + + // 9. If sgn0(u) != sgn0(y), set y = -y + let y = F::conditional_select( + &(-y), + &y, + (u.get_lower_32() % 2).ct_eq(&(y.get_lower_32() % 2)), + ); + + I::Projective::new_jacobian(num_x * div, y * div3, div).unwrap() + } + + /// Implements a degree 3 isogeny map. + fn iso_map(&self, p: &I::Projective) -> C::Projective { + // The input and output are in Jacobian coordinates, using the method + // in "Avoiding inversions" [WB2019, section 4.3]. + + let iso = self.isogeny_constants; + let (x, y, z) = p.jacobian_coordinates(); + + let z2 = z.square(); + let z3 = z2 * z; + let z4 = z2.square(); + let z6 = z3.square(); + + let num_x = ((iso[0] * x + iso[1] * z2) * x + iso[2] * z4) * x + iso[3] * z6; + let div_x = (z2 * x + iso[4] * z4) * x + iso[5] * z6; + + let num_y = (((iso[6] * x + iso[7] * z2) * x + iso[8] * z4) * x + iso[9] * z6) * y; + let div_y = (((x + iso[10] * z2) * x + iso[11] * z4) * x + iso[12] * z6) * z3; + + let zo = div_x * div_y; + let xo = num_x * div_y * zo; + let yo = num_y * div_x * zo.square(); + + C::Projective::new_jacobian(xo, yo, zo).unwrap() + } + + fn field_elements_to_curve(&self, u0: &C::Base, u1: &C::Base) -> C::Projective { + let q0 = self.map_to_curve(u0); + let q1 = self.map_to_curve(u1); + let r: I::Projective = q0 + &q1; + assert!(bool::from(r.is_on_curve())); + // here is where we would scale by the cofactor if we supported nonprime-order curves + self.iso_map(&r) + } +} diff --git a/src/pasta/curves.rs b/src/pasta/curves.rs index 23d0fcb..57ee84c 100644 --- a/src/pasta/curves.rs +++ b/src/pasta/curves.rs @@ -3,6 +3,7 @@ use core::cmp; use core::fmt::Debug; +use core::hash::{Hash, Hasher}; use core::ops::{Add, Mul, Neg, Sub}; use ff::Field; use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; @@ -11,7 +12,8 @@ use super::{Fp, Fq}; use crate::arithmetic::{Curve, CurveAffine, FieldExt, Group}; macro_rules! new_curve_impl { - ($name:ident, $name_affine:ident, $base:ident, $scalar:ident, $blake2b_personalization:literal) => { + ($name:ident, $name_affine:ident, $base:ident, $scalar:ident, $blake2b_personalization:literal, + $curve_id:literal, $a_raw:expr, $b_raw:expr, $curve_type:ident) => { /// Represents a point in the projective coordinate space. #[derive(Copy, Clone, Debug)] pub struct $name { @@ -21,9 +23,12 @@ macro_rules! new_curve_impl { } impl $name { + const fn curve_constant_a() -> $base { + $base::from_raw($a_raw) + } + const fn curve_constant_b() -> $base { - // NOTE: this is specific to b = 5 - $base::from_raw([5, 0, 0, 0]) + $base::from_raw($b_raw) } } @@ -51,6 +56,8 @@ macro_rules! new_curve_impl { type Scalar = $scalar; type Base = $base; + impl_projective_curve_specific!($name, $base, $curve_type); + fn zero() -> Self { Self { x: $base::zero(), @@ -59,19 +66,6 @@ macro_rules! new_curve_impl { } } - fn one() -> Self { - // NOTE: This is specific to b = 5 - - const NEGATIVE_ONE: $base = $base::neg(&$base::one()); - const TWO: $base = $base::from_raw([2, 0, 0, 0]); - - Self { - x: NEGATIVE_ONE, - y: TWO, - z: $base::one(), - } - } - fn is_zero(&self) -> Choice { self.z.ct_is_zero() } @@ -92,56 +86,27 @@ macro_rules! new_curve_impl { $name_affine::conditional_select(&tmp, &$name_affine::zero(), zinv.ct_is_zero()) } - fn double(&self) -> Self { - // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l - // - // There are no points of order 2. - - let a = self.x.square(); - let b = self.y.square(); - let c = b.square(); - let d = self.x + b; - let d = d.square(); - let d = d - a - c; - let d = d + d; - let e = a + a + a; - let f = e.square(); - let z3 = self.z * self.y; - let z3 = z3 + z3; - let x3 = f - (d + d); - let c = c + c; - let c = c + c; - let c = c + c; - let y3 = e * (d - x3) - c; - - let tmp = $name { - x: x3, - y: y3, - z: z3, - }; - - $name::conditional_select(&tmp, &$name::zero(), self.is_zero()) - } - - /// Apply the curve endomorphism by multiplying the x-coordinate - /// by an element of multiplicative order 3. - fn endo(&self) -> Self { - $name { - x: self.x * $base::ZETA, - y: self.y, - z: self.z, - } + fn a() -> Self::Base { + $name::curve_constant_a() } fn b() -> Self::Base { $name::curve_constant_b() } - fn is_on_curve(&self) -> Choice { - // Y^2 - X^3 = 5(Z^6) + fn jacobian_coordinates(&self) -> ($base, $base, $base) { + (self.x, self.y, self.z) + } - (self.y.square() - (self.x.square() * self.x)) - .ct_eq(&((self.z.square() * self.z).square() * $name::curve_constant_b())) + fn is_on_curve(&self) -> Choice { + // Y^2 = X^3 + AX(Z^4) + b(Z^6) + // Y^2 - (X^2 + A(Z^4))X = b(Z^6) + + let z2 = self.z.square(); + let z4 = z2.square(); + let z6 = z4 * z2; + (self.y.square() - (self.x.square() + $name::curve_constant_a() * z4) * self.x) + .ct_eq(&(z6 * $name::curve_constant_b())) | self.z.ct_is_zero() } @@ -182,6 +147,11 @@ macro_rules! new_curve_impl { *q = $name_affine::conditional_select(&q, &$name_affine::zero(), skip); } } + + fn new_jacobian(x: Self::Base, y: Self::Base, z: Self::Base) -> CtOption { + let p = $name { x, y, z }; + CtOption::new(p, p.is_on_curve()) + } } impl<'a> From<&'a $name_affine> for $name { @@ -508,6 +478,9 @@ macro_rules! new_curve_impl { type Base = $base; const BLAKE2B_PERSONALIZATION: &'static [u8; 16] = $blake2b_personalization; + const CURVE_ID: &'static str = $curve_id; + + impl_affine_curve_specific!($name, $base, $curve_type); fn zero() -> Self { Self { @@ -517,26 +490,13 @@ macro_rules! new_curve_impl { } } - fn one() -> Self { - // NOTE: This is specific to b = 5 - - const NEGATIVE_ONE: $base = $base::neg(&$base::from_raw([1, 0, 0, 0])); - const TWO: $base = $base::from_raw([2, 0, 0, 0]); - - Self { - x: NEGATIVE_ONE, - y: TWO, - infinity: Choice::from(0u8), - } - } - fn is_zero(&self) -> Choice { self.infinity } fn is_on_curve(&self) -> Choice { - // y^2 - x^3 ?= b - (self.y.square() - (self.x.square() * self.x)).ct_eq(&$name::curve_constant_b()) + // y^2 - x^3 - ax ?= b + (self.y.square() - (self.x.square() + &$name::curve_constant_a()) * self.x).ct_eq(&$name::curve_constant_b()) | self.infinity } @@ -636,6 +596,10 @@ macro_rules! new_curve_impl { } } + fn a() -> Self::Base { + $name::curve_constant_a() + } + fn b() -> Self::Base { $name::curve_constant_b() } @@ -686,6 +650,14 @@ macro_rules! new_curve_impl { } } + impl Hash for $name_affine { + fn hash(&self, state: &mut H) { + self.x.hash(state); + self.y.hash(state); + bool::from(self.infinity).hash(state) + } + } + impl_binops_additive!($name, $name); impl_binops_additive!($name, $name_affine); impl_binops_additive_specify_output!($name_affine, $name_affine, $name); @@ -712,5 +684,177 @@ macro_rules! new_curve_impl { }; } -new_curve_impl!(Ep, EpAffine, Fp, Fq, b"halo2_____pallas"); -new_curve_impl!(Eq, EqAffine, Fq, Fp, b"halo2______vesta"); +macro_rules! impl_projective_curve_specific { + ($name:ident, $base:ident, special_a0_b5) => { + fn one() -> Self { + // NOTE: This is specific to b = 5 + + const NEGATIVE_ONE: $base = $base::neg(&$base::one()); + const TWO: $base = $base::from_raw([2, 0, 0, 0]); + + Self { + x: NEGATIVE_ONE, + y: TWO, + z: $base::one(), + } + } + + /// Apply the curve endomorphism by multiplying the x-coordinate + /// by an element of multiplicative order 3. + fn endo(&self) -> Self { + $name { + x: self.x * $base::ZETA, + y: self.y, + z: self.z, + } + } + + fn double(&self) -> Self { + // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l + // + // There are no points of order 2. + + let a = self.x.square(); + let b = self.y.square(); + let c = b.square(); + let d = self.x + b; + let d = d.square(); + let d = d - a - c; + let d = d + d; + let e = a + a + a; + let f = e.square(); + let z3 = self.z * self.y; + let z3 = z3 + z3; + let x3 = f - (d + d); + let c = c + c; + let c = c + c; + let c = c + c; + let y3 = e * (d - x3) - c; + + let tmp = $name { + x: x3, + y: y3, + z: z3, + }; + + $name::conditional_select(&tmp, &$name::zero(), self.is_zero()) + } + }; + ($name:ident, $base:ident, general) => { + /// Unimplemented: there is no standard generator for this curve. + fn one() -> Self { + unimplemented!() + } + + /// Unimplemented: no endomorphism is supported for this curve. + fn endo(&self) -> Self { + unimplemented!() + } + + fn double(&self) -> Self { + // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl + // + // There are no points of order 2. + + let xx = self.x.square(); + let yy = self.y.square(); + let a = yy.square(); + let zz = self.z.square(); + let s = (self.x + yy).square() - xx - a; + let s = s + s; + let m = xx + xx + xx + $name::curve_constant_a() * zz.square(); + let x3 = m.square() - (s + s); + let a = a + a; + let a = a + a; + let a = a + a; + let y3 = m * (s - x3) - a; + let z3 = (self.x + self.y).square() - yy - zz; + + let tmp = $name { + x: x3, + y: y3, + z: z3, + }; + + $name::conditional_select(&tmp, &$name::zero(), self.is_zero()) + } + }; +} + +macro_rules! impl_affine_curve_specific { + ($name:ident, $base:ident, special_a0_b5) => { + fn one() -> Self { + // NOTE: This is specific to b = 5 + + const NEGATIVE_ONE: $base = $base::neg(&$base::from_raw([1, 0, 0, 0])); + const TWO: $base = $base::from_raw([2, 0, 0, 0]); + + Self { + x: NEGATIVE_ONE, + y: TWO, + infinity: Choice::from(0u8), + } + } + }; + ($name:ident, $base:ident, general) => { + /// Unimplemented: there is no standard generator for this curve. + fn one() -> Self { + unimplemented!() + } + }; +} + +new_curve_impl!( + Ep, + EpAffine, + Fp, + Fq, + b"halo2_____pallas", + "pallas", + [0, 0, 0, 0], + [5, 0, 0, 0], + special_a0_b5 +); +new_curve_impl!( + Eq, + EqAffine, + Fq, + Fp, + b"halo2______vesta", + "vesta", + [0, 0, 0, 0], + [5, 0, 0, 0], + special_a0_b5 +); +new_curve_impl!( + IsoEp, + IsoEpAffine, + Fp, + Fq, + b"halo2_iso_pallas", + "iso-pallas", + [ + 0x92bb4b0b657a014b, + 0xb74134581a27a59f, + 0x49be2d7258370742, + 0x18354a2eb0ea8c9c, + ], + [1265, 0, 0, 0], + general +); +new_curve_impl!( + IsoEq, + IsoEqAffine, + Fq, + Fp, + b"halo2__iso_vesta", + "iso-vesta", + [ + 0xc515ad7242eaa6b1, + 0x9673928c7d01b212, + 0x81639c4d96f78773, + 0x267f9b2ee592271a, + ], + [1265, 0, 0, 0], + general +); diff --git a/src/pasta/fields/fp.rs b/src/pasta/fields/fp.rs index 737789c..c43fe2c 100644 --- a/src/pasta/fields/fp.rs +++ b/src/pasta/fields/fp.rs @@ -16,7 +16,7 @@ use crate::arithmetic::{adc, mac, sbb, FieldExt, Group, SqrtTables}; // The internal representation of this type is four 64-bit unsigned // integers in little-endian order. `Fp` values are always in // Montgomery form; i.e., Fp(a) = aR mod p, with R = 2^256. -#[derive(Clone, Copy, Eq)] +#[derive(Clone, Copy, Eq, Hash)] pub struct Fp(pub(crate) [u64; 4]); impl fmt::Debug for Fp { diff --git a/src/pasta/fields/fq.rs b/src/pasta/fields/fq.rs index 56e73c7..544fcb6 100644 --- a/src/pasta/fields/fq.rs +++ b/src/pasta/fields/fq.rs @@ -16,7 +16,7 @@ use crate::arithmetic::{adc, mac, sbb, FieldExt, Group, SqrtTables}; // The internal representation of this type is four 64-bit unsigned // integers in little-endian order. `Fq` values are always in // Montgomery form; i.e., Fq(a) = aR mod q, with R = 2^256. -#[derive(Clone, Copy, Eq)] +#[derive(Clone, Copy, Eq, Hash)] pub struct Fq(pub(crate) [u64; 4]); impl fmt::Debug for Fq { diff --git a/src/pasta/pallas.rs b/src/pasta/pallas.rs index 78b2f64..0b7e249 100644 --- a/src/pasta/pallas.rs +++ b/src/pasta/pallas.rs @@ -1,13 +1,182 @@ -//! The Pallas elliptic curve group. +//! The Pallas and iso-Pallas elliptic curve groups. + +use lazy_static::lazy_static; + +use super::{Ep, EpAffine, Fp, Fq, IsoEp, IsoEpAffine}; +use crate::arithmetic::{FieldExt, SimplifiedSWUWithDegree3Isogeny}; + +/// The base field of the Pallas and iso-Pallas curves. +pub type Base = Fp; + +/// The scalar field of the Pallas and iso-Pallas curves. +pub type Scalar = Fq; /// A Pallas point in the projective coordinate space. -pub type Point = super::Ep; +pub type Point = Ep; /// A Pallas point in the affine coordinate space (or the point at infinity). -pub type Affine = super::EpAffine; +pub type Affine = EpAffine; -/// The base field of the Pallas group. -pub type Base = super::Fp; +/// An iso-Pallas point in the projective coordinate space. +pub type IsoPoint = IsoEp; -/// The scalar field of the Pallas group. -pub type Scalar = super::Fq; +/// A iso-Pallas point in the affine coordinate space (or the point at infinity). +pub type IsoAffine = IsoEpAffine; + +lazy_static! { + /// The iso-Pallas -> Pallas degree 3 isogeny map. + pub static ref MAP: SimplifiedSWUWithDegree3Isogeny = { + let isogeny_constants: [Base; 13] = [ + Base::from_raw([ + 0x775f6034aaaaaaab, + 0x4081775473d8375b, + 0xe38e38e38e38e38e, + 0x0e38e38e38e38e38, + ]), + Base::from_raw([ + 0x8cf863b02814fb76, + 0x0f93b82ee4b99495, + 0x267c7ffa51cf412a, + 0x3509afd51872d88e, + ]), + Base::from_raw([ + 0x0eb64faef37ea4f7, + 0x380af066cfeb6d69, + 0x98c7d7ac3d98fd13, + 0x17329b9ec5253753, + ]), + Base::from_raw([ + 0xeebec06955555580, + 0x8102eea8e7b06eb6, + 0xc71c71c71c71c71c, + 0x1c71c71c71c71c71, + ]), + Base::from_raw([ + 0xc47f2ab668bcd71f, + 0x9c434ac1c96b6980, + 0x5a607fcce0494a79, + 0x1d572e7ddc099cff, + ]), + Base::from_raw([ + 0x2aa3af1eae5b6604, + 0xb4abf9fb9a1fc81c, + 0x1d13bf2a7f22b105, + 0x325669becaecd5d1, + ]), + Base::from_raw([ + 0x5ad985b5e38e38e4, + 0x7642b01ad461bad2, + 0x4bda12f684bda12f, + 0x1a12f684bda12f68, + ]), + Base::from_raw([ + 0xc67c31d8140a7dbb, + 0x07c9dc17725cca4a, + 0x133e3ffd28e7a095, + 0x1a84d7ea8c396c47, + ]), + Base::from_raw([ + 0x02e2be87d225b234, + 0x1765e924f7459378, + 0x303216cce1db9ff1, + 0x3fb98ff0d2ddcadd, + ]), + Base::from_raw([ + 0x93e53ab371c71c4f, + 0x0ac03e8e134eb3e4, + 0x7b425ed097b425ed, + 0x025ed097b425ed09, + ]), + Base::from_raw([ + 0x5a28279b1d1b42ae, + 0x5941a3a4a97aa1b3, + 0x0790bfb3506defb6, + 0x0c02c5bcca0e6b7f, + ]), + Base::from_raw([ + 0x4d90ab820b12320a, + 0xd976bbfabbc5661d, + 0x573b3d7f7d681310, + 0x17033d3c60c68173, + ]), + Base::from_raw([ + 0x992d30ecfffffde5, + 0x224698fc094cf91b, + 0x0000000000000000, + 0x4000000000000000, + ]), + ]; + + let z = -Base::from_u64(13); + SimplifiedSWUWithDegree3Isogeny::new(&z, &isogeny_constants) + }; +} + +#[test] +fn test_iso_map() { + use crate::arithmetic::{Curve, HashToCurve}; + + // This is a regression test (it's the same input to iso_map as for hash_to_curve + // with domain prefix "z.cash:test", Shake128, and input b"hello"). + let r = IsoPoint::new_jacobian( + Base::from_raw([ + 0xc37f111df5c4419e, + 0x593c053e5e2337ad, + 0x9c6cfc47bce1aba6, + 0x0a881e4d556945aa, + ]), + Base::from_raw([ + 0xf234e04434502b47, + 0x6979f7f2b0acf188, + 0xa62eec46f662cb4e, + 0x035e5c8a06d5cfb4, + ]), + Base::from_raw([ + 0x11ab791d4fb6f6b4, + 0x575baa717958ef1f, + 0x6ac4e343558dcbf3, + 0x3af37975b0933125, + ]), + ) + .unwrap(); + let p = MAP.iso_map(&r); + let (x, y, z) = p.jacobian_coordinates(); + assert!( + format!("{:?}", x) == "0x318cc15f281662b3f26d0175cab97b924870c837879cac647e877be51a85e898" + ); + assert!( + format!("{:?}", y) == "0x1e91e2fa2a5a6a5bc86ff9564ae9336084470e7119dffcb85ae8c1383a3defd7" + ); + assert!( + format!("{:?}", z) == "0x1e049436efa754f5f189aec69c2c3a4a559eca6a12b45c3f2e4a769deeca6187" + ); +} + +#[test] +fn test_map_to_curve_pallas() { + use crate::arithmetic::{Curve, CurveAffine, HashToCurve, Shake128}; + use std::collections::HashSet; + + assert!(MAP.minus_b_over_a * IsoAffine::a() == -IsoAffine::b()); + assert!(MAP.b_over_za * MAP.z * IsoAffine::a() == IsoAffine::b()); + assert!(MAP.theta.square() * Base::ROOT_OF_UNITY == MAP.z); + + let set: HashSet<_> = (0..10000) + .map(|i| MAP.map_to_curve(&Base::from(i)).to_affine()) + .collect(); + assert!(set.len() == 10000); + + let hash = MAP.hash_to_curve("z.cash:test", Shake128::default()); + let p: Point = hash(b"hello"); + let (x, y, z) = p.jacobian_coordinates(); + println!("{:?}", p); + assert!( + format!("{:?}", x) == "0x318cc15f281662b3f26d0175cab97b924870c837879cac647e877be51a85e898" + ); + assert!( + format!("{:?}", y) == "0x1e91e2fa2a5a6a5bc86ff9564ae9336084470e7119dffcb85ae8c1383a3defd7" + ); + assert!( + format!("{:?}", z) == "0x1e049436efa754f5f189aec69c2c3a4a559eca6a12b45c3f2e4a769deeca6187" + ); +} diff --git a/src/pasta/vesta.rs b/src/pasta/vesta.rs index c20d9b6..c867fee 100644 --- a/src/pasta/vesta.rs +++ b/src/pasta/vesta.rs @@ -1,13 +1,142 @@ -//! The Vesta elliptic curve group. +//! The Vesta and iso-Vesta elliptic curve groups. + +use lazy_static::lazy_static; + +use super::{Eq, EqAffine, Fp, Fq, IsoEq, IsoEqAffine}; +use crate::arithmetic::{FieldExt, SimplifiedSWUWithDegree3Isogeny}; + +/// The base field of the Vesta and iso-Vesta curves. +pub type Base = Fq; + +/// The scalar field of the Vesta and iso-Vesta curves. +pub type Scalar = Fp; /// A Vesta point in the projective coordinate space. -pub type Point = super::Eq; +pub type Point = Eq; /// A Vesta point in the affine coordinate space (or the point at infinity). -pub type Affine = super::EqAffine; +pub type Affine = EqAffine; -/// The base field of the Vesta group. -pub type Base = super::Fq; +/// An iso-Vesta point in the projective coordinate space. +pub type IsoPoint = IsoEq; -/// The scalar field of the Vesta group. -pub type Scalar = super::Fp; +/// A iso-Vesta point in the affine coordinate space (or the point at infinity). +pub type IsoAffine = IsoEqAffine; + +lazy_static! { + /// The iso-Vesta -> Vesta degree 3 isogeny map. + pub static ref MAP: SimplifiedSWUWithDegree3Isogeny = { + let isogeny_constants: [Base; 13] = [ + Base::from_raw([ + 0x43cd42c800000001, + 0x0205dd51cfa0961a, + 0x8e38e38e38e38e39, + 0x38e38e38e38e38e3, + ]), + Base::from_raw([ + 0x8b95c6aaf703bcc5, + 0x216b8861ec72bd5d, + 0xacecf10f5f7c09a2, + 0x1d935247b4473d17, + ]), + Base::from_raw([ + 0xaeac67bbeb586a3d, + 0xd59d03d23b39cb11, + 0xed7ee4a9cdf78f8f, + 0x18760c7f7a9ad20d, + ]), + Base::from_raw([ + 0xfb539a6f0000002b, + 0xe1c521a795ac8356, + 0x1c71c71c71c71c71, + 0x31c71c71c71c71c7, + ]), + Base::from_raw([ + 0xb7284f7eaf21a2e9, + 0xa3ad678129b604d3, + 0x1454798a5b5c56b2, + 0x0a2de485568125d5, + ]), + Base::from_raw([ + 0xf169c187d2533465, + 0x30cd6d53df49d235, + 0x0c621de8b91c242a, + 0x14735171ee542778, + ]), + Base::from_raw([ + 0x6bef1642aaaaaaab, + 0x5601f4709a8adcb3, + 0xda12f684bda12f68, + 0x12f684bda12f684b, + ]), + Base::from_raw([ + 0x8bee58e5fb81de63, + 0x21d910aefb03b31d, + 0xd6767887afbe04d1, + 0x2ec9a923da239e8b, + ]), + Base::from_raw([ + 0x4986913ab4443034, + 0x97a3ca5c24e9ea63, + 0x66d1466e9de10e64, + 0x19b0d87e16e25788, + ]), + Base::from_raw([ + 0x8f64842c55555533, + 0x8bc32d36fb21a6a3, + 0x425ed097b425ed09, + 0x1ed097b425ed097b, + ]), + Base::from_raw([ + 0x58dfecce86b2745e, + 0x06a767bfc35b5bac, + 0x9e7eb64f890a820c, + 0x2f44d6c801c1b8bf, + ]), + Base::from_raw([ + 0xd43d449776f99d2f, + 0x926847fb9ddd76a1, + 0x252659ba2b546c7e, + 0x3d59f455cafc7668, + ]), + Base::from_raw([ + 0x8c46eb20fffffde5, + 0x224698fc0994a8dd, + 0x0000000000000000, + 0x4000000000000000, + ]), + ]; + + let z = -Base::from_u64(13); + SimplifiedSWUWithDegree3Isogeny::new(&z, &isogeny_constants) + }; +} + +#[test] +fn test_map_to_curve_vesta() { + use crate::arithmetic::{Curve, CurveAffine, HashToCurve, Shake128}; + use std::collections::HashSet; + + assert!(MAP.minus_b_over_a * IsoAffine::a() == -IsoAffine::b()); + assert!(MAP.b_over_za * MAP.z * IsoAffine::a() == IsoAffine::b()); + assert!(MAP.theta.square() * Base::ROOT_OF_UNITY == MAP.z); + + let set: HashSet<_> = (0..10000) + .map(|i| MAP.map_to_curve(&Base::from(i)).to_affine()) + .collect(); + assert!(set.len() == 10000); + + let hash = MAP.hash_to_curve("z.cash:test", Shake128::default()); + let p: Point = hash(b"hello"); + let (x, y, z) = p.jacobian_coordinates(); + println!("{:?}", p); + assert!( + format!("{:?}", x) == "0x3984612258b3b43b4f6e046f7f796bbd35ffd8908804bcf47b9537d3ec7645c9" + ); + assert!( + format!("{:?}", y) == "0x2573c035293d745a288a65a7a37709ef99bcf31b77cfb3a1126a61e3adeebc4b" + ); + assert!( + format!("{:?}", z) == "0x1cb99da94a634842b09a3ee1e5b462233e1fc23d0b357ec7fb0d1c409be30720" + ); +}