diff --git a/Cargo.toml b/Cargo.toml index 1472723..c31c123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,15 @@ name = "dkg" path = "examples/dkg.rs" [dependencies] -hex-literal = "0.4.1" num-traits = "0.2.19" subtle = "2.6.1" crypto-bigint = "0.6.0-rc.2" sha3 = "0.11.0-pre.4" +lazy_static = "1.5.0" [dev-dependencies] +hex-literal = "0.4.1" serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" sha2 = "0.11.0-pre.4" diff --git a/README.md b/README.md index 6ed8e45..efeca32 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# sylow +![Logo](./sylow.png) [![License](https://img.shields.io/crates/l/sylow)](https://choosealicense.com/licenses/mit/) [![Crates.io](https://img.shields.io/crates/v/sylow)](https://crates.io/crates/sylow) [![Docs](https://img.shields.io/crates/v/sylow?color=blue&label=docs)](https://docs.rs/sylow/) ![CI](https://github.com/warlock-labs/sylow/actions/workflows/CI.yml/badge.svg) -sylow is a Rust library implementing the BLS (Boneh-Lynn-Shacham) signature scheme using the alt-bn128 (BN254) elliptic curve. It provides threshold signing capabilities and associated utilities, initially developed for use in the Warlock Chaos product. +Sylow (*ˈsyːlɔv*) is a Rust library providing functionality for signature generation and verification using +the alt-bn128 (BN 254) elliptic curve, initially developed for use in the Warlock Chaos product. It provides a +general finite field implementation, and extends it for usage into groups on the relevant elliptic curves of alt-bn128. ## Features -- Implementation of BLS signatures on the alt-bn128 (BN254) curve -- Support for threshold signatures - Efficient pairing operations leveraging the alt-bn128 curve's properties - Utilities for key generation, signing, and verification - Compatibility with Ethereum's precompiled contracts for alt-bn128 operations @@ -45,9 +45,10 @@ For more examples and usage details, see the [API documentation](https://docs.rs ## Core Concepts -- **BLS Signatures**: A signature scheme allowing for signature aggregation and threshold signing. +- **Finite fields**: These serve as the backbone of modern cryptography, allowing for secure signature schemes. - **alt-bn128 (BN254) Curve**: An elliptic curve with efficient pairing operations, widely used in zkSNARKs and supported by Ethereum precompiles. -- **Threshold Signatures**: A cryptographic primitive allowing a subset of parties to collaboratively sign messages. +- **Optimal ate pairing**: A cryptographic primitive allowing for efficient computation to verify the validity of a + cryptographic signature. ## Performance @@ -57,11 +58,9 @@ The alt-bn128 curve is chosen for its efficiency and widespread support, particu The following features and improvements are planned for future releases: -- [ ] Basic BLS signature implementation -- [ ] Key generation utilities -- [ ] Signature aggregation -- [ ] Threshold signature scheme -- [ ] Optimizations for common operations +- [x] Basic signature implementation +- [x] Key generation utilities +- [x] Optimizations for common operations - [ ] Extended test suite and benchmarks - [ ] Support for serialization formats used in blockchain contexts @@ -75,6 +74,10 @@ This project is licensed under the [MIT License](https://choosealicense.com/lice ## Contact +This project is maintained by: +- Tristan Britt [tristan@warlock.xyz](mailto:tristan@warlock.xyz) +- 0xAlcibiades [alcibiades@warlock.xyz](mailto:alcibiades@warlock.xyz) + Warlock Labs - [https://github.com/warlock-labs](https://github.com/warlock-labs) Project Link: [https://github.com/warlock-labs/sylow](https://github.com/warlock-labs/sylow) \ No newline at end of file diff --git a/examples/epoch.rs b/examples/epoch.rs new file mode 100644 index 0000000..29b88ec --- /dev/null +++ b/examples/epoch.rs @@ -0,0 +1,28 @@ +//! This example shows how to leverage the batch computation of the Miller loops, or otherwise +//! reuse the same G2 element in the pairing in repeated verifications. +use crypto_bigint::rand_core::OsRng; +use subtle::ConstantTimeEq; +use sylow::{ + pairing, FieldExtensionTrait, Fp, Fr, G1Affine, G1Projective, G2Projective, GroupTrait, +}; + +fn main() { + // First, let's generate a shared secret ... + let private_key = Fp::new(Fr::rand(&mut OsRng).value()); + // ... and a public key from it, at which we evaluate the coefficients of the Miller loops + let pubkey = (G2Projective::generator() * private_key).precompute(); + // Now, imagine we have 10 signatures we wish to verify. + let hashed_msgs: Vec = (0..10).map(|_| G1Affine::rand(&mut OsRng)).collect(); + + let signatures: Vec = hashed_msgs + .iter() + .map(|x| G1Projective::from(x) * private_key) + .collect(); + // We can evaluate each of them individually using the precomputed coefficients ... + for (sig, msg) in signatures.iter().zip(hashed_msgs.iter()) { + let lhs = pairing(sig, &G2Projective::generator()); + let rhs = pubkey.miller_loop(msg).final_exponentiation(); + assert!(bool::from(lhs.ct_eq(&rhs))); + } + println!("All signatures are valid!"); +} diff --git a/src/fields/extensions.rs b/src/fields/extensions.rs index a959dae..90881dc 100644 --- a/src/fields/extensions.rs +++ b/src/fields/extensions.rs @@ -34,12 +34,16 @@ impl> FieldExtensio /// This is a const constructor that takes a slice of field elements and returns a field extension /// The usage of the generics means that it is possible to instantiate any representation of /// an extension need. + /// # Arguments + /// * `c` - a slice of field elements pub(crate) const fn new(c: &[F; N]) -> Self { Self(*c) } /// There is eventually a need to be able to perform multiplication across different field /// extensions, and more or less this corresponds to a basic scaling, see /// + /// # Arguments + /// * `factor` - a field element that is used to scale the extension element pub(crate) fn scale(&self, factor: F) -> Self { let mut i = 0; let mut retval = [F::zero(); N]; diff --git a/src/fields/fp.rs b/src/fields/fp.rs index 47145ff..fffeb50 100644 --- a/src/fields/fp.rs +++ b/src/fields/fp.rs @@ -83,12 +83,15 @@ pub trait FieldExtensionTrait: + Inv + From { - // multiplication in a field extension is dictated - // heavily such a value below + /// multiplication in a field extension is dictated heavily such a value below, + /// so we define the quadratic non-residue here fn quadratic_non_residue() -> Self; - + /// generate a random value in the field extension based on the random number generator from + /// `crypto_bigint` fn rand(rng: &mut R) -> Self; - + /// because each extension is directly used in a j-invariant 0 curve, we define the constant + /// of that curve over the extension field. Namely, it is the value `b` in the equation + /// `y^2=x^3+b`. fn curve_constant() -> Self; } /// Visibility settings in rust on macro exports make this seem as not use, even though its @@ -107,6 +110,19 @@ where /// This means that we roll our implementation into a proc macro that /// provides all the needed functionality. +/// This macro defines a finite prime field and implements various traits and methods for it. +/// +/// # Parameters +/// +/// * `$wrapper_name`: The name of the wrapper struct for the field. +/// * `$mod_struct`: The name of the modulus struct. +/// * `$output`: The name of the output type for Montgomery form. +/// * `$uint_type`: The underlying unsigned integer type used for the field elements. +/// * `$limbs`: The number of limbs in the underlying unsigned integer type. +/// * `$modulus`: The modulus of the field as a string. +/// * `$degree`: The degree of the field extension. +/// * `$nreps`: The number of elements required for a unique representation of an element in the +/// extension. #[allow(unused_macros)] macro_rules! define_finite_prime_field { ($wrapper_name:ident, $mod_struct:ident, $output:ident, $uint_type:ty, $limbs:expr, @@ -118,15 +134,24 @@ macro_rules! define_finite_prime_field { //special struct for const-time arithmetic on montgomery form integers mod p type $output = crypto_bigint::modular::ConstMontyForm<$mod_struct, { $mod_struct::LIMBS }>; #[derive(Clone, Debug, Copy)] //to be used in const contexts + /// This is the actual struct that serves as our finite field implementation, containing + /// the modulus of the field, as well as the output type that contains the internal + /// Montgomery arithmetic logic pub struct $wrapper_name($mod_struct, $output); impl FinitePrimeField<$limbs, $uint_type, $degree, $nreps> for $wrapper_name {} impl $wrapper_name { - // builder structure to create elements in the base field of a given value + /// builder structure to create elements in the base field + /// # Arguments + /// * `value` - $uint_type - the value to create the element from pub const fn new(value: $uint_type) -> Self { Self($mod_struct, $output::new(&value)) } + /// builder structure to create elements in the base field from a string + /// representation of the value in base 10 + /// # Arguments + /// * `value` - &str - the string representation of the value to create the element from pub fn new_from_str(value: &str) -> Option { let ints: Vec<_> = { let mut acc = Self::zero(); @@ -150,18 +175,25 @@ macro_rules! define_finite_prime_field { } Some(res) } - // take the element and convert it to "normal" form from montgomery form + /// take the element and convert it to "normal" form from montgomery form pub const fn value(&self) -> $uint_type { self.1.retrieve() } + /// returns the value of the finite field modulus as a $uint_type pub fn characteristic() -> $uint_type { <$uint_type>::from($mod_struct::MODULUS.as_nz_ref().get()) } + /// the constant zero in the field pub const ZERO: Self = Self::new(<$uint_type>::from_words([0x0; 4])); + /// the constant one in the field pub const ONE: Self = Self::new(<$uint_type>::from_words([0x1, 0x0, 0x0, 0x0])); + /// the constant two in the field pub const TWO: Self = Self::new(<$uint_type>::from_words([0x2, 0x0, 0x0, 0x0])); + /// the constant three in the field pub const THREE: Self = Self::new(<$uint_type>::from_words([0x3, 0x0, 0x0, 0x0])); + /// the constant four in the field pub const FOUR: Self = Self::new(<$uint_type>::from_words([0x4, 0x0, 0x0, 0x0])); + /// the constant nine in the field pub const NINE: Self = Self::new(<$uint_type>::from_words([0x9, 0x0, 0x0, 0x0])); } // we make the base field an extension of the @@ -173,6 +205,9 @@ macro_rules! define_finite_prime_field { // = -1 Self::new((-Self::ONE).1.retrieve()) } + /// Generate a random value in the field + /// # Arguments + /// * `rng` - R: CryptoRngCore - the random number generator to use fn rand(rng: &mut R) -> Self { Self::new(<$uint_type>::random_mod( rng, @@ -187,6 +222,8 @@ macro_rules! define_finite_prime_field { } } impl From for $wrapper_name { + // many often there is a need to create a simple value like `3` in the base field, + // which is what this accomplishes fn from(value: u64) -> Self { Self($mod_struct, $output::new(&<$uint_type>::from_u64(value))) } @@ -401,37 +438,42 @@ impl From for Fp { } } impl Fp { + /// This determines the frobenius mapping of the element in the base field, aka x^p. This + /// function is inherently expensive, and we never call it on the base field, but if + /// we did, it's only defined for p=1. Specialized versions exist for all extensions which + /// will require the frobenius transformation. pub fn frobenius(&self, exponent: usize) -> Self { - // this function is inherently expensive, and we never call it on the base field, but if - // we did, it's only defined for p=1. Specialized versions exist for all extensions which - // will require the frobenius transformation match exponent { 1 => self.pow(BN254_FP_MODULUS.value()), _ => *self, } } + /// This is an instantiation of Shank's algorithm, which solves congruences of + /// the form $r^2\equiv n \mod p$, namely the sqrt of n. It does not work for + /// composite moduli (aka non-prime p), since that is the integer factorization + /// problem. The full algorithm is not necessary here, and has the additional + /// simplification that we can exploit in our case. Namely, the BN254 curve has a + /// prime that is congruent to 3 mod 4. In this case, the sqrt only has the + /// possible solution of $\pm pow(n, \frac{p+1}{4})$, which is where this magic + /// number below comes from ;) pub fn sqrt(&self) -> CtOption { - // This is an instantiation of Shank's algorithm, which solves congruences of - // the form $r^2\equiv n \mod p$, namely the sqrt of n. It does not work for - // composite moduli (aka nonprime p), since that is the integer factorization - // problem. The full algorithm is not necessary here, and has the additional - // simpication that we can exploit in our case. Namely, the BN254 curve has a - // prime that is congruent to 3 mod 4. In this case, the sqrt only has the - // possible solution of $\pm pow(n, \frac{p+1}{4})$, which is where this magic - // number below comes from ;) let arg = ((Self::new(Self::characteristic()) + Self::one()) / Self::from(4)).value(); let sqrt = self.pow(arg); CtOption::new(sqrt, sqrt.square().ct_eq(self)) } + /// Returns the square of the element in the base field pub fn square(&self) -> Self { (*self) * (*self) } + /// Determines if the element in the base field is a square of another element pub fn is_square(&self) -> Choice { let p_minus_1_div_2 = ((Self::new(Self::characteristic()) - Self::from(1)) / Self::from(2)).value(); let retval = self.pow(p_minus_1_div_2); Choice::from((retval == Self::zero() || retval == Self::one()) as u8) } + /// Determines the 'sign' of a value in the base field, + /// see for more details pub fn sgn0(&self) -> Choice { let a = *self % Self::from(2u64); if a.is_zero() { diff --git a/src/fields/fp12.rs b/src/fields/fp12.rs index 2b7b63f..c3d42a8 100644 --- a/src/fields/fp12.rs +++ b/src/fields/fp12.rs @@ -1,4 +1,4 @@ -//! we likewise define the specifics of the dodectic extension of +//! We likewise define the specifics of the dodectic extension of //! bn254 here, defined by the tower F_{p^{12}} = F_{p^6}(w) / (w^2 - v) //! Now, there is some flexibility in how we define this. Why? //! Well, we can either represent an element of F_{p^{12}} as 2 elements @@ -189,7 +189,7 @@ impl FieldExtensionTrait<12, 2> for Fp12 { ]) } fn curve_constant() -> Self { - unimplemented!() + Self::from(3) } } @@ -197,7 +197,8 @@ impl<'a, 'b> Mul<&'b Fp12> for &'a Fp12 { type Output = Fp12; fn mul(self, other: &'b Fp12) -> Self::Output { // this is again simple Karatsuba multiplication - // see comments in Fp2 impl of `Mul` trait + // see comments in Fp2 impl of `Mul` trait, or otherwise see Alg 20 of + // let t0 = self.0[0] * other.0[0]; let t1 = self.0[1] * other.0[1]; @@ -221,6 +222,7 @@ impl MulAssign for Fp12 { impl Inv for Fp12 { type Output = Self; fn inv(self) -> Self::Output { + // Implements Alg 23 of let tmp = (self.0[0].square() - (self.0[1].square().residue_mul())).inv(); Self([self.0[0] * tmp, -(self.0[1] * tmp)]) } @@ -287,6 +289,14 @@ impl Fp12 { /// The function below is called by `zcash`, `bn`, and `arkworks` as `mul_by_024`, referring to /// the indices of the non-zero elements in the 6x Fp2 representation above for the /// multiplication. + /// + /// # Arguments + /// * `ell_0` - Fp2, the first entry of the sparse element + /// * `ell_vw` - Fp2, the second entry of the sparse element + /// * `ell_vv` - Fp2, the third entry of the sparse element + /// + /// # Returns + /// * A dense Fp12 element pub(crate) fn sparse_mul(&self, ell_0: Fp2, ell_vw: Fp2, ell_vv: Fp2) -> Fp12 { let z0 = self.0[0].0[0]; let z1 = self.0[0].0[1]; diff --git a/src/fields/fp2.rs b/src/fields/fp2.rs index 3cb9a1e..d7c1acb 100644 --- a/src/fields/fp2.rs +++ b/src/fields/fp2.rs @@ -51,6 +51,13 @@ pub type Fp2 = FieldExtension<2, 2, Fp>; // helper functions for us on this specific extension, but // don't generalize to any extension. impl Fp2 { + /// A simple square and multipy algorithm for exponentiation + /// # Arguments + /// * `by` - Fp, the exponent to raise the element to + /// + /// Note that the argument is required to be an element of the base field, and the expansion + /// of this element via `to_words()` always returns &[u64; 4], which lets this run constant time + /// for any field element. pub fn pow(&self, by: &Fp) -> Self { let bits = by.value().to_words(); let mut res = Self::one(); @@ -68,6 +75,9 @@ impl Fp2 { pub(crate) fn residue_mul(&self) -> Self { self * &FP2_QUADRATIC_NON_RESIDUE } + /// Frobenius mapping of a quadratic extension element to a given power + /// # Arguments + /// * `exponent` - usize, the power to raise the element to pub fn frobenius(&self, exponent: usize) -> Self { let frobenius_coeff_fp2: &[Fp; 2] = &[ // Fp::quadratic_non_residue()**(((p^0) - 1) / 2) diff --git a/src/fields/fp6.rs b/src/fields/fp6.rs index d2de000..7da0f74 100644 --- a/src/fields/fp6.rs +++ b/src/fields/fp6.rs @@ -254,6 +254,7 @@ impl MulAssign for Fp6 { impl Inv for Fp6 { type Output = Self; fn inv(self) -> Self::Output { + // Implements a low-overhead version of Alg 17 of let t0 = self.0[0].square() - self.0[1] * self.0[2].residue_mul(); let t1 = self.0[2].square().residue_mul() - self.0[0] * self.0[1]; let t2 = self.0[1].square() - self.0[0] * self.0[2]; diff --git a/src/fields/utils.rs b/src/fields/utils.rs index 46a85be..64d05dd 100644 --- a/src/fields/utils.rs +++ b/src/fields/utils.rs @@ -1,5 +1,15 @@ use crypto_bigint::{Encoding, U256, U512}; +/// This function is used to convert a smaller byte array to a larger one +/// It's mainly useful for upcasting arithmetic. For example, in order to compute p^2 in +/// non-modular arithmetic, having p as a U256 will cause overflow in p^2, so we up-cast it toa +/// U512, and then do the squaring to contain the result. The below simply does this conversion +/// for a given input and output dimension. +/// # Generics +/// * `N` - the size of the input slice +/// * `M` - the size of the output slice +/// # Arguments +/// * `smaller_bytes` - a slice of bytes that is to be converted to a larger slice pub(crate) fn to_larger_uint(smaller_bytes: &[u8; N]) -> [u8; M] { assert!(M > N, "Target size must be larger than source size"); let mut larger_bytes = [0u8; M]; @@ -7,6 +17,7 @@ pub(crate) fn to_larger_uint(smaller_bytes: &[u8 larger_bytes } +/// A specific instantiation of casting from U256 to U512, used in the hashing operations // Specific conversion functions pub(crate) fn u256_to_u512(u256: &U256) -> U512 { U512::from_be_bytes(to_larger_uint::<32, 64>(&u256.to_be_bytes())) diff --git a/src/groups/g1.rs b/src/groups/g1.rs index ab62945..77a43ec 100644 --- a/src/groups/g1.rs +++ b/src/groups/g1.rs @@ -18,9 +18,10 @@ use crypto_bigint::rand_core::CryptoRngCore; use num_traits::Zero; use subtle::{Choice, ConstantTimeEq}; -pub(crate) type G1Affine = GroupAffine<1, 1, Fp>; - -pub(crate) type G1Projective = GroupProjective<1, 1, Fp>; +/// type alias for affine representation on base field +pub type G1Affine = GroupAffine<1, 1, Fp>; +/// type alias for projective representation on base field +pub type G1Projective = GroupProjective<1, 1, Fp>; impl GroupTrait<1, 1, Fp> for G1Affine { fn generator() -> Self { @@ -51,10 +52,6 @@ impl GroupTrait<1, 1, Fp> for G1Affine { Err(e) => Err(e), } } - /// NOTA BENE: the frobenius map does NOT in general map points from the curve back to the curve - /// It is an endomorphism of the algebraic closure of the base field, but NOT of the curve - /// Therefore, these points must bypass curve membership and torsion checks, and therefore - /// directly be instantiated as a struct fn frobenius(&self, exponent: usize) -> Self { let vec: Vec = [self.x, self.y] .iter() @@ -69,9 +66,12 @@ impl GroupTrait<1, 1, Fp> for G1Affine { } impl G1Affine { - /// this needs to be defined in order to have user interaction, but currently - /// is only visible in tests, and therefore is seen by the linter as unused - pub fn new(v: [Fp; 2]) -> Result { + // Instantiate a new element in affine coordinates in G1. The input values must simply pass + // the curve check, since the r-torsion of the curve on the base field is the entire curve + // and therefore no subgroup check is required in G1. + // # Arguments + // * `v` - a tuple of field elements that represent the x and y coordinates of the point + fn new(v: [Fp; 2]) -> Result { let _g1affine_is_on_curve = |x: &Fp, y: &Fp, z: &Choice| -> Choice { let y2 = y.square(); let x2 = x.square(); @@ -112,6 +112,11 @@ impl GroupTrait<1, 1, Fp> for G1Projective { fn rand(rng: &mut R) -> Self { Self::generator() * >::rand(rng) } + /// There are two steps in the process of taking a byte array and putting it to an element in + /// the group. First, hash the array to a string into two elements from the base field using + /// the `expand_msg` standard, and map each of these to an element of the group, and then add + /// those group elements to arrive at the final hash, see `hasher.rs` and `svdw.rs` for more + /// details. fn hash_to_curve(exp: &E, msg: &[u8]) -> Result { const COUNT: usize = 2; const L: usize = 48; @@ -154,7 +159,12 @@ impl GroupTrait<1, 1, Fp> for G1Projective { } } impl G1Projective { - pub fn new(v: [Fp; 3]) -> Result { + /// Instantiate a new element in projective coordinates in G1. The input values must simply pass + /// the curve check, since the r-torsion of the curve on the base field is the entire curve. + /// # Arguments + /// * `v` - a tuple of field elements that represent the x, y, and z coordinates of the point + #[allow(dead_code)] + pub(crate) fn new(v: [Fp; 3]) -> Result { let _g1projective_is_on_curve = |x: &Fp, y: &Fp, z: &Fp| -> Choice { let y2 = y.square(); let x2 = x.square(); diff --git a/src/groups/g2.rs b/src/groups/g2.rs index a239daf..a833e28 100644 --- a/src/groups/g2.rs +++ b/src/groups/g2.rs @@ -89,8 +89,9 @@ pub(crate) const EPS_EXP1: Fp2 = Fp2::new(&[ // the parameter that generates this member of the BN family pub(crate) const BLS_X: Fp = Fp::new(U256::from_words([4965661367192848881, 0, 0, 0])); -pub(crate) type G2Affine = GroupAffine<2, 2, Fp2>; - +/// type alias for affine representation on quadratic extension field +pub type G2Affine = GroupAffine<2, 2, Fp2>; +/// type alias for projective representation on quadratic extension field pub type G2Projective = GroupProjective<2, 2, Fp2>; impl GroupTrait<2, 2, Fp2> for G2Affine { @@ -105,29 +106,7 @@ impl GroupTrait<2, 2, Fp2> for G2Affine { infinity: Choice::from(0u8), } } - /// This is deceptively simple, yet was tedious to get correct. This is the - /// "untwist-Frobenius-twist" endomorphism, ψ(q) = u o π o u⁻¹ where u:E'→E is the - /// isomorphism - /// from the twist to the curve E and π is the Frobenius map. This is a complicated - /// topic, and a full - /// description is out of scope for a code comment. Nonetheless, we require the usage of - /// an endomorphism for subgroup checks in $\mathbb{G}_2$. This one offers nice - /// computational - /// benefits, and can be decomposed as follows: - /// 1. twist: this is the map u that takes (x', y') |-> (w^2,x', w^3y'), where - /// $w\in\mathbb{F_{p^{12}}$ is a root of $X^6-\xi$. This is an - /// injective map (that is not surjective), that maps the r-torsion to an - /// equivalent subgroup in the algebraic closure of the base field. - /// 2. Frobenius: this is the map π that is difficult to succinctly explain, but more or - /// less identifies the kernel of the twist operation, namely those points - /// in the algebraic closure that satisfy rQ=0. - /// 3. untwist: having identified the points in the closure that satisfy the r-torsion - /// requirement, we map them back to the curve in Fp2. - /// - /// This endomorphism therefore encodes the torsion of a point in a compact, - /// computationally efficient way. All of this fancy stuff equates simply, and remarkably, - /// to the following: - /// (x,y) |-> (x^p * \xi^((p-1)/3), y^p*\xi^((p-1)/2)) + fn endomorphism(&self) -> Self { if self.is_zero() { return *self; @@ -145,6 +124,9 @@ impl GroupTrait<2, 2, Fp2> for G2Affine { Self::from(G2Projective::rand(rng)) } + /// Being able to implement a "G1/G2 swap" is in development, where we then will hash a byte + /// array to G2 (private key + signature in G2), while retaining a public key in G1, which is + /// why the following two methods are unimplemented for the moment. fn hash_to_curve(_exp: &E, _msg: &[u8]) -> Result { unimplemented!() } @@ -212,10 +194,6 @@ impl GroupTrait<2, 2, Fp2> for G2Projective { ) -> Result { unimplemented!() } - /// NOTA BENE: the frobenius map does NOT in general map points from the curve back to the curve - /// It is an endomorphism of the algebraic closure of the base field, but NOT of the curve - /// Therefore, these points must bypass curve membership and torsion checks, and therefore - /// directly be instantiated as a struct fn frobenius(&self, exponent: usize) -> Self { let vec: Vec = [self.x, self.y, self.z] .iter() @@ -232,9 +210,12 @@ impl G2Affine { /// This method is used internally for rapid, low overhead, conversion of types when there /// are formulae that don't have clean versions in projective coordinates. The 'unchecked' /// refers to the fact that these points are not subjected to a subgroup verification, and - /// therefore this method is not exposed pub(crate) licly. + /// therefore this method is not exposed publicly. /// /// DON'T USE THIS METHOD UNLESS YOU KNOW WHAT YOU'RE DOING + /// + /// # Arguments + /// * `v` - a tuple of field elements that represent the x and y coordinates of the point fn new_unchecked(v: [Fp2; 2]) -> Result { let _g2affine_is_on_curve = |x: &Fp2, y: &Fp2, z: &Choice| -> Choice { let y2 = y.square(); @@ -256,10 +237,13 @@ impl G2Affine { } } impl G2Projective { - /// The pub(crate) lic entrypoint to making a value in $\mathbb{G}_2$. This takes the (x,y,z) values + /// The public entrypoint to making a value in $\mathbb{G}_2$. This takes the (x,y,z) values /// from the user, and passes them through a subgroup and curve check to ensure validity. /// Values returned from this function are guaranteed to be on the curve and in the r-torsion. - pub fn new(v: [Fp2; 3]) -> Result { + /// + /// # Arguments + /// * `v` - a tuple of field elements that represent the x, y, and z coordinates of the point + pub(crate) fn new(v: [Fp2; 3]) -> Result { let _g2projective_is_on_curve = |x: &Fp2, y: &Fp2, z: &Fp2| -> Choice { let y2 = y.square(); let x2 = x.square(); diff --git a/src/groups/group.rs b/src/groups/group.rs index 625b8af..0b61c30 100644 --- a/src/groups/group.rs +++ b/src/groups/group.rs @@ -26,14 +26,17 @@ use crypto_bigint::rand_core::CryptoRngCore; use crypto_bigint::subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; use std::ops::{Add, Mul, Neg, Sub}; +/// This is a simple error struct that specifies the three errors +/// that are expected for the generation of a point on the curve. +/// Either, the coordinates given are not even on the curve, +/// or they are not in the correct subgroup, aka the r-torsion. #[derive(Debug, Copy, Clone)] pub enum GroupError { - /// This is a simple error struct that specifies the three errors - /// that are expected for the generation of a point on the curve. - /// Either, the coordinates given are not even on the curve, - /// or they are not in the correct subgroup, aka the r-torsion. + /// if the point is not on the curve NotOnCurve, + /// if the point is not in the r-torsion subgroup NotInSubgroup, + /// if the point cannot be hashed to the group CannotHashToGroup, } @@ -47,17 +50,66 @@ pub trait GroupTrait Self; - /// required for subgroup checks + /// This is deceptively simple, yet was tedious to get correct. This is the + /// "untwist-Frobenius-twist" endomorphism, ψ(q) = u o π o u⁻¹ where u:E'→E is the + /// isomorphism + /// from the twist to the curve E and π is the Frobenius map. This is a complicated + /// topic, and a full + /// description is out of scope for a code comment. Nonetheless, we require the usage of + /// an endomorphism for subgroup checks in $\mathbb{G}_2$. This one offers nice + /// computational + /// benefits, and can be decomposed as follows: + /// 1. twist: this is the map u that takes (x', y') |-> (w^2,x', w^3y'), where + /// $w\in\mathbb{F_{p^{12}}$ is a root of $X^6-\xi$. This is an + /// injective map (that is not surjective), that maps the r-torsion to an + /// equivalent subgroup in the algebraic closure of the base field. + /// 2. Frobenius: this is the map π that is difficult to succinctly explain, but more or + /// less identifies the kernel of the twist operation, namely those points + /// in the algebraic closure that satisfy rQ=0. + /// 3. untwist: having identified the points in the closure that satisfy the r-torsion + /// requirement, we map them back to the curve in Fp2. + /// + /// This endomorphism therefore encodes the torsion of a point in a compact, + /// computationally efficient way. All of this fancy stuff equates simply, and remarkably, + /// to the following: + /// (x,y) |-> (x^p * \xi^((p-1)/3), y^p*\xi^((p-1)/2)) + /// + /// Note that this function will only be meaningful implemented to G2 elements. fn endomorphism(&self) -> Self; - /// generate a random point on the curve + /// Generate a random point on the curve + /// # Arguments + /// * `rng` - a cryptographic random number generator fn rand(rng: &mut R) -> Self; + /// Hash a message to a point on the curve using the `expand_msg` and SvdW standards provided + /// by RFC 9380, see `hasher.rs` and `svdw.rs` for more details. Takes an input message, and + /// expander, and returns an element in the group. + /// # Arguments + /// * `exp` - an object that implements the `Expander` trait, used to hash the message to a + /// point on the curve + /// * `msg` - a slice of bytes that is to be hashed to a point on the curve + /// # Returns + /// * `Result` - a point on the curve, otherwise an error fn hash_to_curve(exp: &E, msg: &[u8]) -> Result; + /// Take an input message, and produce a cryptographic signature on it in the group. + /// # Arguments + /// * `exp` - an object that implements the `Expander` trait, used to hash the message to a + /// point on the curve + /// * `msg` - a slice of bytes that is to be hashed to a point on the curve + /// * `private_key` - a scalar in the base field that is used to sign the message fn sign_message(exp: &E, msg: &[u8], private_key: F) -> Result; /// NOTA BENE: the frobenius map does NOT in general map points from the curve back to the curve /// It is an endomorphism of the algebraic closure of the base field, but NOT of the curve /// Therefore, these points must bypass curve membership and torsion checks, and therefore /// directly be instantiated as a struct + /// + /// This performs the operation: + /// (x,y) |-> (x^p, y^p) + /// + /// # Arguments + /// * `exponent` - usize, the exponent to raise the point to #[allow(dead_code)] fn frobenius(&self, exponent: usize) -> Self; } @@ -78,7 +130,8 @@ pub struct GroupAffine Neg for &'a Gt { type Output = Gt; @@ -226,7 +227,7 @@ impl GroupTrait<12, 2, Fp12> for Gt { } impl Gt { /// Returns the group identity, which is $1$. - pub(crate) fn identity() -> Gt { + pub fn identity() -> Gt { Gt(Fp12::one()) } diff --git a/src/groups/mod.rs b/src/groups/mod.rs index aa0c08c..1ec0bba 100644 --- a/src/groups/mod.rs +++ b/src/groups/mod.rs @@ -17,21 +17,21 @@ pub(crate) mod gt; /// this means each group operation takes sub millisecond time, which is nice. #[cfg(test)] mod tests { + use lazy_static::lazy_static; + use serde::{Deserialize, Serialize}; #[allow(unused_imports)] use std::{fs, path::Path}; - use serde::{Deserialize, Serialize}; - use crate::fields::fp::{FieldExtensionTrait, Fp}; use crate::fields::fp2::Fp2; use crate::groups::g1::{G1Affine, G1Projective}; use crate::groups::g2::G2Projective; - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, Clone)] struct _G2Coords { c0: String, c1: String, } - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, Clone)] struct _G2Projective { x: _G2Coords, y: _G2Coords, @@ -161,107 +161,96 @@ mod tests { } const FNAME: &str = "./src/sage_reference/bn254_reference.json"; - /// Loading from disk is not a const operation (so we therefore cannot just run this code - /// once in the beginning of the `tests` module definition), so we roll the loading - /// and processing into a macro so that it can be performed at the beginning of each test as - /// needed. - /// - macro_rules! load_reference_data_from_disk { - ($wrapper_name:ident) => { + lazy_static! { + static ref REFERENCE_DATA: ReferenceData = { let path = Path::new(FNAME); let file_content = fs::read_to_string(path).expect("Failed to read file"); - let $wrapper_name: ReferenceData = - serde_json::from_str(&file_content).expect("Failed to parse JSON"); + serde_json::from_str(&file_content).expect("Failed to parse JSON") }; - } - macro_rules! load_g1_reference_data { - ($g1_wrapper_name:ident) => { - load_reference_data_from_disk!(reference_data); - let $g1_wrapper_name: G1ReferenceData = G1ReferenceData { - a: reference_data - .g1 - .a - .iter() - .map(convert_to_g1projective) - .collect(), - b: reference_data - .g1 - .b - .iter() - .map(convert_to_g1projective) - .collect(), - r: reference_data.g1.r.iter().map(convert_to_fp).collect(), - add: reference_data - .g1 - .add - .iter() - .map(convert_to_g1projective) - .collect(), - dbl: reference_data - .g1 - .dbl - .iter() - .map(convert_to_g1projective) - .collect(), - mul: reference_data - .g1 - .mul - .iter() - .map(convert_to_g1projective) - .collect(), - svdw: reference_data - .g1 - .svdw - .iter() - .map(convert_to_g1svdw) - .collect(), - }; + static ref G1_REFERENCE_DATA: G1ReferenceData = G1ReferenceData { + a: REFERENCE_DATA + .g1 + .a + .iter() + .map(convert_to_g1projective) + .collect(), + b: REFERENCE_DATA + .g1 + .b + .iter() + .map(convert_to_g1projective) + .collect(), + r: REFERENCE_DATA.g1.r.iter().map(convert_to_fp).collect(), + add: REFERENCE_DATA + .g1 + .add + .iter() + .map(convert_to_g1projective) + .collect(), + dbl: REFERENCE_DATA + .g1 + .dbl + .iter() + .map(convert_to_g1projective) + .collect(), + mul: REFERENCE_DATA + .g1 + .mul + .iter() + .map(convert_to_g1projective) + .collect(), + svdw: REFERENCE_DATA + .g1 + .svdw + .iter() + .map(convert_to_g1svdw) + .collect(), }; - } - macro_rules! load_g2_reference_data { - ($g2_wrapper_name:ident, $g2_invalids:ident) => { - load_reference_data_from_disk!(reference_data); - let $g2_wrapper_name: G2ReferenceData = G2ReferenceData { - a: reference_data - .g2 - .a - .iter() - .map(convert_to_g2projective) - .collect(), - b: reference_data - .g2 - .b - .iter() - .map(convert_to_g2projective) - .collect(), - r: reference_data.g2.r.iter().map(convert_to_fp).collect(), - add: reference_data - .g2 - .add - .iter() - .map(convert_to_g2projective) - .collect(), - dbl: reference_data - .g2 - .dbl - .iter() - .map(convert_to_g2projective) - .collect(), - mul: reference_data - .g2 - .mul - .iter() - .map(convert_to_g2projective) - .collect(), - psi: reference_data - .g2 - .psi - .iter() - .map(convert_to_g2projective) - .collect(), - }; - let $g2_invalids = reference_data.g2.invalid; + static ref G2_REFERENCE_DATA: G2ReferenceData = G2ReferenceData { + a: REFERENCE_DATA + .g2 + .a + .iter() + .map(convert_to_g2projective) + .collect(), + b: REFERENCE_DATA + .g2 + .b + .iter() + .map(convert_to_g2projective) + .collect(), + r: REFERENCE_DATA.g2.r.iter().map(convert_to_fp).collect(), + add: REFERENCE_DATA + .g2 + .add + .iter() + .map(convert_to_g2projective) + .collect(), + dbl: REFERENCE_DATA + .g2 + .dbl + .iter() + .map(convert_to_g2projective) + .collect(), + mul: REFERENCE_DATA + .g2 + .mul + .iter() + .map(convert_to_g2projective) + .collect(), + psi: REFERENCE_DATA + .g2 + .psi + .iter() + .map(convert_to_g2projective) + .collect(), }; + static ref G2_INVALIDS: Vec<_G2Projective> = REFERENCE_DATA + .g2 + .invalid + .iter() + .map(|x| (*x).clone()) + .collect(); } mod g1 { @@ -271,12 +260,12 @@ mod tests { #[test] fn test_generation_and_conversion() { - load_g1_reference_data!(_g1_points); + let _g1_points = &*G1_REFERENCE_DATA; } #[test] #[should_panic(expected = "Conversion to projective failed: NotOnCurve")] fn test_malformed_points() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; for a in &g1_points.a { let mut x = a.x; let y = a.y; @@ -323,7 +312,7 @@ mod tests { #[test] fn test_addition_closure() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; for i in &g1_points.a[1..] { let _ = i + &g1_points.a[0]; } @@ -331,7 +320,7 @@ mod tests { #[test] fn test_addition_associativity_commutativity() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; if let [a, b, c] = &g1_points.a[0..3] { assert_eq!(&(a + b) + c, a + &(b + c), "Addition is not associative"); assert_eq!(a + b, b + a, "Addition is not commutative"); @@ -339,8 +328,8 @@ mod tests { } #[test] fn test_addition_cases() { - load_g1_reference_data!(g1_points); - let expected = g1_points.add; + let g1_points = &*G1_REFERENCE_DATA; + let expected = &g1_points.add; for (i, (a, b)) in g1_points.a.iter().zip(&g1_points.b).enumerate() { let result = a + b; assert_eq!(result, expected[i], "Simple addition failed"); @@ -361,7 +350,7 @@ mod tests { // successful addition test case run, to verify the accuracy of subtraction #[test] fn test_subtraction_closure() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; let a = &g1_points.a[0]; for i in &g1_points.a { let _ = i - a; @@ -371,7 +360,7 @@ mod tests { } #[test] fn test_subtraction_associativity() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; if let [a, b, c] = &g1_points.a[0..3] { assert_eq!(a - &(b - c), &(a - b) + c, "Subtraction is not associative"); } @@ -384,7 +373,7 @@ mod tests { #[test] fn test_doubling() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; for i in &g1_points.a { assert_eq!(i.double(), i + i, "Doubling failed"); } @@ -392,7 +381,7 @@ mod tests { #[test] fn test_scalar_mul() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; let three = Fp::from(3); for i in &g1_points.a { assert_eq!(i + &(i + i), i * &three, "Multiplication failed"); @@ -408,13 +397,13 @@ mod tests { } #[test] fn test_multiplication_cases() { - load_g1_reference_data!(g1_points); - let expected = g1_points.mul; + let g1_points = &*G1_REFERENCE_DATA; + let expected = &g1_points.mul; for (i, (a, r)) in g1_points.a.iter().zip(&g1_points.r).enumerate() { let result = a * r; assert_eq!(result, expected[i], "Simple multiplication failed"); } - let expected = g1_points.dbl; + let expected = &g1_points.dbl; for (i, a) in g1_points.a.iter().enumerate() { let result = a.double(); assert_eq!(result, expected[i], "Simple doubling failed"); @@ -460,7 +449,7 @@ mod tests { #[test] fn test_svdw() { - load_g1_reference_data!(g1_points); + let g1_points = &*G1_REFERENCE_DATA; if let Ok(d) = SvdW::precompute_constants( Fp::ZERO, @@ -486,19 +475,20 @@ mod tests { #[test] fn test_generation_and_conversion() { - load_g2_reference_data!(_g2_points, _g2_invalids); + let _g2_points = &*G2_REFERENCE_DATA; } #[test] #[should_panic(expected = "g2 failed: NotInSubgroup")] fn invalid_subgroup_check() { - load_g2_reference_data!(_g2_points, g2_invalids); + let _g2_points = &*G2_REFERENCE_DATA; + let g2_invalids = &*G2_INVALIDS; let _p: Vec = g2_invalids.iter().map(convert_to_g2projective).collect(); } #[test] #[should_panic(expected = "Endomorphism failed: NotOnCurve")] fn test_malformed_points() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; for a in &g2_points.a { let mut x = a.x; let y = a.y; @@ -547,7 +537,7 @@ mod tests { #[test] fn test_addition_closure() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; for i in &g2_points.a[1..] { let _ = i + &g2_points.a[0]; } @@ -555,7 +545,7 @@ mod tests { #[test] fn test_addition_associativity_commutativity() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; if let [a, b, c] = &g2_points.a[0..3] { assert_eq!(&(a + b) + c, a + &(b + c), "Addition is not associative"); assert_eq!(a + b, b + a, "Addition is not commutative"); @@ -563,8 +553,8 @@ mod tests { } #[test] fn test_addition_cases() { - load_g2_reference_data!(g2_points, _g2_invalids); - let expected = g2_points.add; + let g2_points = &*G2_REFERENCE_DATA; + let expected = &g2_points.add; for (i, (a, b)) in g2_points.a.iter().zip(&g2_points.b).enumerate() { let result = a + b; assert_eq!(result, expected[i], "Simple addition failed"); @@ -572,7 +562,7 @@ mod tests { } #[test] fn test_addition_edge_cases() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; let zero = &G2Projective::zero(); assert_eq!(zero + &g2_points.a[0], g2_points.a[0], "Adding zero failed"); } @@ -581,7 +571,7 @@ mod tests { use super::*; #[test] fn test_subtraction_closure() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; let a = &g2_points.a[0]; for i in &g2_points.a { let _ = i - a; @@ -591,7 +581,7 @@ mod tests { } #[test] fn test_subtraction_associativity() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; if let [a, b, c] = &g2_points.a[0..3] { assert_eq!(a - &(b - c), &(a - b) + c, "Subtraction is not associative"); } @@ -603,7 +593,7 @@ mod tests { #[test] fn test_doubling() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; for i in &g2_points.a { assert_eq!(i.double(), i + i, "Doubling failed"); } @@ -611,7 +601,7 @@ mod tests { #[test] fn test_scalar_mul() { - load_g2_reference_data!(g2_points, _g2_invalids); + let g2_points = &*G2_REFERENCE_DATA; let three = Fp::from(3); for i in &g2_points.a { assert_eq!(i + &(i + i), i * &three, "Multiplication failed"); @@ -619,13 +609,13 @@ mod tests { } #[test] fn test_multiplication_cases() { - load_g2_reference_data!(g2_points, _g2_invalids); - let expected = g2_points.mul; + let g2_points = &*G2_REFERENCE_DATA; + let expected = &g2_points.mul; for (i, (a, r)) in g2_points.a.iter().zip(&g2_points.r).enumerate() { let result = a * r; assert_eq!(result, expected[i], "Simple multiplication failed"); } - let expected = g2_points.dbl; + let expected = &g2_points.dbl; for (i, a) in g2_points.a.iter().enumerate() { let result = a.double(); assert_eq!(result, expected[i], "Simple doubling failed"); @@ -645,8 +635,8 @@ mod tests { #[test] fn test_psi() { - load_g2_reference_data!(g2_points, _g2_invalids); - let expected = g2_points.psi; + let g2_points = &*G2_REFERENCE_DATA; + let expected = &g2_points.psi; for (i, a) in g2_points.a.iter().enumerate() { let result = a.endomorphism(); assert_eq!(result, expected[i], "Endomorphic mapping failed"); diff --git a/src/hasher.rs b/src/hasher.rs index 1d119f2..f892d67 100644 --- a/src/hasher.rs +++ b/src/hasher.rs @@ -8,24 +8,52 @@ use crypto_bigint::{Encoding, NonZero, U256, U512}; use sha3::digest::crypto_common::BlockSizeUser; use sha3::digest::{ExtendableOutput, FixedOutput}; use std::array::TryFromSliceError; -#[derive(Debug)] -pub(crate) enum HashError { +#[derive(Debug, Copy, Clone)] +pub enum HashError { CastToField, ExpandMessage, ConvertInt, } +/// This is a simple integer to octet representation of the given length conversion tool. +/// # Arguments +/// * `val` - the integer to be converted +/// * `length` - the length of the output octet string +/// # Returns +/// * a Result containing the octet string or an error if the conversion fails fn i2osp(val: u64, length: usize) -> Result, HashError> { - // this is an integer to octet representation of the given length if val >= (1 << (8 * length)) { return Err(HashError::ConvertInt); } Ok(val.to_be_bytes()[8 - length..].to_vec()) } -pub(crate) trait Expander { + +/// The suggested way to generate a value in a base field from a byte array is to use a technique +/// called message expansion, as described by RFC 9380, see +/// . This is a trait that +/// must be satisfied for any version of this standard. +pub trait Expander { + // If the domain separation tag is above 255 characters, then this prefix must be added as + // required by the standard. const OVERSIZE_DST_PREFIX: &'static [u8] = b"H2C-OVERSIZE-DST-"; + // Actually performs the message expansion to the target length in bytes. + // # Arguments + // * `msg` - the message to be expanded + // * `len_in_bytes` - the length of the output in bytes + // # Returns + // * a Result containing the expanded message or an error if the expansion fails fn expand_message(&self, msg: &[u8], len_in_bytes: usize) -> Result, HashError>; + // This function is used to convert a byte array to a field element. The standard technically + // defines this function to work for any field extension degree, and allows for the + // partitioning of the expanded message into multiple field elements. For our cases here, + // we're interested in the base field (degree=1), and two elements of 48 bytes each. + // # Arguments + // * `msg` - the message to be expanded + // * `count` - the number of field elements to be returned + // * `size` - the size of each field element in bytes + // # Returns + // * a Result containing the field elements or an error if the conversion fails fn hash_to_field(&self, msg: &[u8], count: usize, size: usize) -> Result<[Fp; 2], HashError> { // const COUNT: usize = 2; // const L: usize = 48; @@ -37,12 +65,20 @@ pub(crate) trait Expander { for (i, f) in retval.iter_mut().enumerate() { let elm_offset = size * i; let tv = &exp_msg[elm_offset..elm_offset + size]; + // this just simply copies the relevant slice of bytes cleanly into a full 64-byte slice let mut bs = [0u8; 64]; bs[16..].copy_from_slice(tv); + // the next step requires taking the value of current chunk of bytes and modulo'ing + // it by the base field order. However, because the slice of the expanded message was + // fit into a 64-byte array (bigger than the definition of our modulus = 256 = 32 + // bytes), we have to up-cast our modulus to a U512 to perform the arithmetic let cast_value = U512::from_be_bytes(bs); let modulus = NonZero::::new(u256_to_u512(&Fp::characteristic())).unwrap(); + // since the leading bytes of the expanded message slice are 0 anyway, the first 4 + // words of the value will ALWAYS be [0x0,0x0,0x0,0x0], so we can truncate safely to + // get the relevant values let scalar = U256::from_words( (cast_value % modulus).to_words()[0..4] .try_into() @@ -65,6 +101,12 @@ pub(crate) struct XMDExpander { } impl XMDExpander { + /// Generate a new instance of the expander based on a domain separation tag, and desired bit + /// level of security. For BN254, this is in theory 128, but has been shown recently to be + /// ~100, see . + /// # Arguments + /// * `dst` - the domain separation tag + /// * `security_param` - the desired bit level of security pub(crate) fn new(dst: &[u8], security_param: u64) -> Self { let dst_prime = if dst.len() > 255 { let mut hasher = D::default(); diff --git a/src/lib.rs b/src/lib.rs index 4ec84ff..e83b105 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ #![doc = include_str!("../README.md")] #![deny(unsafe_code)] #![deny(dead_code)] -#![allow(clippy::needless_doctest_main)] +#![allow(clippy::needless_doctest_main, clippy::doc_lazy_continuation)] #![warn( clippy::unwrap_used, missing_docs, @@ -20,12 +20,13 @@ mod hasher; mod pairing; mod svdw; -use crate::fields::fp::{FieldExtensionTrait, Fp, Fr}; -use crate::groups::g1::G1Projective; -use crate::groups::g2::G2Projective; -use crate::groups::group::{GroupError, GroupTrait}; +pub use crate::fields::fp::{FieldExtensionTrait, Fp, Fr}; +pub use crate::groups::g1::{G1Affine, G1Projective}; +pub use crate::groups::g2::G2Projective; +pub use crate::groups::group::{GroupError, GroupTrait}; +pub use crate::groups::gt::Gt; use crate::hasher::XMDExpander; -use crate::pairing::pairing; +pub use crate::pairing::{glued_miller_loop, pairing, G2PreComputed}; use crypto_bigint::rand_core::OsRng; use sha3::Keccak256; use subtle::ConstantTimeEq; diff --git a/src/pairing.rs b/src/pairing.rs index 55eff75..c94a3d7 100644 --- a/src/pairing.rs +++ b/src/pairing.rs @@ -22,8 +22,13 @@ const ATE_LOOP_COUNT_NAF: [i8; 64] = [ 1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, -1, 0, -1, 0, 0, 1, 0, 0, 0, -1, 0, 0, -1, 0, 1, 0, 1, 0, 0, 0, ]; + +/// This is mainly a struct on convenience. The whole multiplicative versus additive notation for +/// an element is confusing, so we just enforce that the results of miller loops are handled with +/// multiplication. And semantically, this just helps me keep straight all the different values, +/// base fields, groups, etc., that are involved here. Arithmetic defined by reference. #[derive(Copy, Clone, Debug)] -pub(crate) struct MillerLoopResult(pub(crate) Fp12); +pub struct MillerLoopResult(pub(crate) Fp12); impl Default for MillerLoopResult { fn default() -> Self { MillerLoopResult(Fp12::one()) @@ -70,11 +75,23 @@ impl<'b> MulAssign<&'b MillerLoopResult> for MillerLoopResult { /// which is very, very sparse, resulting in many unnecessary multiplications and additions by /// zero, which is not ideal. We therefore only keep the 3 nonzero coefficients returned by these /// evaluations. These nonzero coeffs are stored in the struct below. -#[derive(PartialEq, Default, Clone, Copy)] +#[derive(PartialEq, Default, Clone, Copy, Debug)] pub(crate) struct Ell(Fp2, Fp2, Fp2); impl MillerLoopResult { - pub(crate) fn final_exponentiation(&self) -> Gt { + /// Indeed performs the final exponentiation step, which equates to f^((p^12-1)/r) in the + /// case of BN254. This is nasty to compute cleverly, since the naive approach is insane for + /// a value that large. Therefore, there are tricks that involve what are known as cyclotomic + /// subgroups that allow us to compute this value in two steps: (i) the "easy" part, and (ii) + /// the "hard" part. The easy part is determining the exponentiation of the value to: + /// (p^6-1)(p^2+1) + /// and the hard part is the exponentiation to: + /// (p^4-p^2+1)/r + /// The hard part is what relies on the cyclotomic subgroups, which is difficult to explain + /// in a code comment. See for more context. + /// + /// This returns an element in the target group, which is the r-th roots of unity in Fp12. + pub fn final_exponentiation(&self) -> Gt { /// As part of the cyclotomic acceleration of the final exponentiation step, there is a /// shortcut to take when using multiplication in Fp4. We built the tower of extensions using /// degrees 2, 6, and 12, but there is an additional way to write Fp12: @@ -139,7 +156,7 @@ impl MillerLoopResult { z3 = z3 + z3 + t2; Fp12::new(&[Fp6::new(&[z0, z4, z3]), Fp6::new(&[z2, z1, z5])]) } - /// This is a simple double and add algorithm for exponentiation. You can get more + /// This is a simple square and multiply algorithm for exponentiation. You can get more /// complicated algorithms if you go to a compressed representation, such as Algorithm /// 5.5.4, listing 27 pub(crate) fn cyclotomic_exp(f: Fp12, exponent: &Fp) -> Fp12 { @@ -155,7 +172,7 @@ impl MillerLoopResult { } res } - /// This is a helper function to determine f^z, where $z$ is the generator of this + /// This is a helper function to determine f^(-z), where $z$ is the generator of this /// particular member of the BN family pub(crate) fn exp_by_neg_z(f: Fp12) -> Fp12 { cyclotomic_exp(f, &BLS_X).unitary_inverse() @@ -226,13 +243,20 @@ impl MillerLoopResult { /// doubling step. Further, there are 9 `1` digits (each with an addition step), and 12 `3` /// digits, each also with an addition step. After the loop, there are 2 more addition steps, so /// the total number of coefficients we need to store is 64+9+12+2 = 87. -#[derive(PartialEq)] -pub(crate) struct G2PreComputed { +#[derive(PartialEq, Debug, Copy, Clone)] +pub struct G2PreComputed { pub(crate) q: G2Affine, pub(crate) coeffs: [Ell; 87], } impl G2PreComputed { - pub(crate) fn miller_loop(&self, g1: &G1Affine) -> MillerLoopResult { + /// Evaluate the miller loop for the pre-determined line coefficients for a given G2 point at + /// the specified G1 coordinate. See for more + /// info on the formulation. + /// # Arguments + /// * `g1` - the G1 point at which to evaluate the line + /// # Returns + /// * the result of the miller loop evaluation + pub fn miller_loop(&self, g1: &G1Affine) -> MillerLoopResult { let mut f = Fp12::one(); let mut idx = 0; @@ -259,8 +283,14 @@ impl G2PreComputed { MillerLoopResult(f) } } +impl G2Projective { + pub fn precompute(&self) -> G2PreComputed { + G2Affine::from(self).precompute() + } +} impl G2Affine { - fn precompute(&self) -> G2PreComputed { + /// For a given G2 coordinate, compute the coefficients of every line involved for that point. + pub fn precompute(&self) -> G2PreComputed { let mut r = G2Projective::from(self); let mut coeffs = [Ell::default(); 87]; @@ -348,7 +378,15 @@ impl G2Projective { ) } } -pub(crate) fn pairing(p: &G1Projective, q: &G2Projective) -> Gt { +/// Execute the optimal ate pairing on BN254 for a given input pair of (G1,G2) points. If either +/// of these points are the point at infinity for their respective groups, then the code handles +/// it gracefully. +/// # Arguments +/// * `p` - the G1 point +/// * `q` - the G2 point +/// # Returns +/// * the result of the pairing +pub fn pairing(p: &G1Projective, q: &G2Projective) -> Gt { let p = &G1Affine::from(p); let q = &G2Affine::from(q); let either_zero = Choice::from((p.is_zero() | q.is_zero()) as u8); @@ -359,10 +397,15 @@ pub(crate) fn pairing(p: &G1Projective, q: &G2Projective) -> Gt { tmp.final_exponentiation() } -pub(crate) fn glued_miller_loop( - g2_precomps: &[G2PreComputed], - g1s: &[G1Affine], -) -> MillerLoopResult { +/// There are many times when we need to evaluate many pairings at the same time. This simply +/// provides the ability to execute an array of pairings as succinctly as possible, for example +/// in the context of threshold signature verification. +/// # Arguments +/// * `g1s` - an array of G1 points +/// * `g2s` - an array of G2 points +/// # Returns +/// * the result of the pairing, doing each one individually and then aggregating their result +pub fn glued_miller_loop(g2_precomps: &[G2PreComputed], g1s: &[G1Affine]) -> MillerLoopResult { let mut f = Fp12::one(); let mut idx = 0; for i in ATE_LOOP_COUNT_NAF.iter() { @@ -392,6 +435,12 @@ pub(crate) fn glued_miller_loop( } MillerLoopResult(f) } +/// The driver code for the glued miller loop execution, see comments above. +/// # Arguments +/// * `g1s` - an array of G1 points +/// * `g2s` - an array of G2 points +/// # Returns +/// * the result of the pairing, doing each one individually and then aggregating their result #[allow(dead_code)] pub(crate) fn glued_pairing(g1s: &[G1Projective], g2s: &[G2Projective]) -> Gt { let g1s = g1s.iter().map(G1Affine::from).collect::>(); diff --git a/src/svdw.rs b/src/svdw.rs index 1752f91..66d1d86 100644 --- a/src/svdw.rs +++ b/src/svdw.rs @@ -36,7 +36,12 @@ pub(crate) trait SvdWTrait: Sized { /// This is the actual struct containing the relevant information. There are a few input /// constants, namely the coefficients A and B that define the curve in its short Weierstrass /// representation. The constants c1-c4 and Z are determined by the algorithm. - + /// # Arguments + /// * `a` - the A coefficient of the curve + /// * `b` - the B coefficient of the curve + /// # Returns + /// * `Result` - the struct containing the constants for the SvdW algorithm, + /// or an error otherwise fn find_z_svdw(a: Fp, b: Fp) -> Fp { let g = |x: &Fp| -> Fp { (*x) * (*x) * (*x) + a * (*x) + b }; let h = |x: &Fp| -> Fp { -(Fp::THREE * (*x) * (*x) + Fp::FOUR * a) / (Fp::FOUR * g(x)) }; @@ -61,7 +66,14 @@ pub(crate) trait SvdWTrait: Sized { ctr += 1; } } - + /// There are a few constants in the SvdW algorithm, and this subroutine actually determines + /// them for the given curve, see Ref 1 for more details. + /// # Arguments + /// * `a` - the A coefficient of the curve + /// * `b` - the B coefficient of the curve + /// # Returns + /// * `Result` - the struct containing the constants for the SvdW algorithm, + /// or an error otherwise fn precompute_constants(a: Fp, b: Fp) -> Result { let g = |x: &Fp| -> Fp { (*x) * (*x) * (*x) + a * (*x) + b }; let z = Self::find_z_svdw(a, b); @@ -93,6 +105,16 @@ pub(crate) trait SvdWTrait: Sized { z, }) } + /// Having determined the constants for the SvdW algorithm, we actually perform the mapping + /// to an element of the group. The issue here is that we do not explicitly check the result + /// of this operation to verify that it satisfies the curve equation, since that + /// functionality is nearly contained in `groups.rs` etc. Therefore, this private method is + /// called by `g1.rs` etc., which then calls its `new` method to perform the subgroup and + /// curve checks, meaning that it is ok for those checks to not occur here. + /// # Arguments + /// * `u` - the scalar to be mapped to a point on the curve + /// # Returns + /// * `Result<[Fp; 2], MapError>` - the point on the curve, or an error otherwise fn unchecked_map_to_point(&self, u: Fp) -> Result<[Fp; 2], MapError>; } impl SvdWTrait for SvdW { diff --git a/sylow.png b/sylow.png new file mode 100644 index 0000000..5b589b6 Binary files /dev/null and b/sylow.png differ