-
Notifications
You must be signed in to change notification settings - Fork 145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimized subgroup checks #580
Conversation
Codecov Report
@@ Coverage Diff @@
## main #580 +/- ##
==========================================
+ Coverage 95.48% 95.52% +0.03%
==========================================
Files 112 110 -2
Lines 19008 19267 +259
==========================================
+ Hits 18149 18404 +255
- Misses 859 863 +4
... and 10 files with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! We'll be reviewing it in the next few days. Left some minor comments of some things I saw.
math/src/elliptic_curve/short_weierstrass/curves/bls12_381/curve.rs
Outdated
Show resolved
Hide resolved
math/src/elliptic_curve/short_weierstrass/curves/bls12_381/curve.rs
Outdated
Show resolved
Hide resolved
math/src/elliptic_curve/short_weierstrass/curves/bls12_381/curve.rs
Outdated
Show resolved
Hide resolved
math/src/elliptic_curve/short_weierstrass/curves/bls12_381/pairing.rs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi! I'm still going through it when I have time. I left some comments based on what I've reviewed so far. Let me know what you think.
let fe: FieldElement<QuadraticExtensionField<MyQuadraticNonResidue>> = | ||
QuadraticExtensionFieldElement::new([elem[0].clone(), elem[1].clone()]); | ||
let frob_map = fe.conjugate(); | ||
|
||
[frob_map.value()[0].clone(), frob_map.value()[1].clone()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep this simple and avoid all those clones.
let fe: FieldElement<QuadraticExtensionField<MyQuadraticNonResidue>> = | |
QuadraticExtensionFieldElement::new([elem[0].clone(), elem[1].clone()]); | |
let frob_map = fe.conjugate(); | |
[frob_map.value()[0].clone(), frob_map.value()[1].clone()] | |
[elem[0].clone(), -&elem[1]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw FieldElement<QuadraticExtensionField>
has conjugate fn. But it constraints to type that implement HasQuadraticNonResidue
. So, I implemented it (Named it so badly). If we can conjugate like you suggested, then why the FieldElement<QuadraticExtensionField>
requires HasQuadraticNonResidue
trait to be implemented to access conjugate fn?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QuadraticExtensionField
is a generic implementation of a quadratic extension for general non quadratic residues. The struct Degree2Extension
is an independent custom impementation just for BLS12 381 to leverage some optimizations coming from the fact that the quadratic non residue is -1. For example, multiplication formulas for quadratic extensions in general involve multiplying by the quadratic non residue. When the residue is -1, there are aimpler formulas and one can avoid multiplying by 1 or -1. That's why for bls 12 381 we don't use the generic quadratic extension implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be explained in the docs of Degree2Extension
. Sorry for that. I bet its confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QuadraticExtensionField
is a generic implementation of a quadratic extension for general non quadratic residues. The structDegree2Extension
is an independent custom impementation just for BLS12 381 to leverage some optimizations coming from the fact that the quadratic non residue is -1. For example, multiplication formulas for quadratic extensions in general involve multiplying by the quadratic non residue. When the residue is -1, there are aimpler formulas and one can avoid multiplying by 1 or -1. That's why for bls 12 381 we don't use the generic quadratic extension implementation.
Thanks a lot for this explanation! Makes a lot more sense now. This is the true learning.
FieldElement::<BLS12381PrimeField>::from_hex_unchecked("0"), | ||
]); | ||
|
||
/// Seed value for BLS-12 381 curve. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Seed value for BLS-12 381 curve. | |
/// Modulus of seed value for BLS-12 381 curve. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got the value from here. There it is mentioned as seed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. For the sake of clarity, let's cite Construction 6.6 of the taxonomy of elliptic curves paper and mention that SEED
is
Check out this site to convert latex to ut8 and produce more legible formulas in code comments, (like x⁴ − x² + 1 instead of x^4 - x^2 + 1)
/// Returns the boolean indicating whether the given field element is zero. | ||
pub fn is_zero(elem: &<Self as IsField>::BaseType) -> bool { | ||
elem[0] == FieldElement::<BLS12381PrimeField>::zero() | ||
&& elem[1] == FieldElement::<BLS12381PrimeField>::zero() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no need to implement this. We already have this behavior with zero
elements and eq
.
/// Returns the boolean indicating whether the given field element is zero. | |
pub fn is_zero(elem: &<Self as IsField>::BaseType) -> bool { | |
elem[0] == FieldElement::<BLS12381PrimeField>::zero() | |
&& elem[1] == FieldElement::<BLS12381PrimeField>::zero() | |
} | |
} |
Degree2ExtensionField::is_zero(psi_plus_seed_times_p.z().value()) | ||
&& BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Degree2ExtensionField::is_zero(psi_plus_seed_times_p.z().value()) | |
&& BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) | |
&FieldElement::zero() == psi_plus_seed_times_p.z() | |
&& BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) |
let psi_plus_seed_times_p = self.psi().operate_with(&seed_times_p); | ||
|
||
Degree2ExtensionField::is_zero(psi_plus_seed_times_p.z().value()) | ||
&& BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If z is zero then the point is already the point at infinity and does belong to the curve. There's no need to check that. Did you mean BLS12381TwistCurve::is_on_curve(self)
here?
Even in that case, self
is already of type ShortWeierstrassProjectivePoint<BLS12381TwistCurve>
, that shouldn't be necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw that's how its implemented in Gnark. I should have listened to it that both checks for same thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually it doesn't really have to do with z
equal to zero implying being equal to the point at infinity (since that already assumes that the point is on the curve). We should be able to rely on the fact that points of type ShortWeierstrassProjectivePoint<BLS12381TwistCurve>
are on the curve. So a method such as is_on_curve
should be redundant for instances of that type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, ShortWeierstrassProjectivePoint
can be made public at the crate level and add a builder fn that checks whether the point is on curve. Will that work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Prolly we only need the two builder functions we already have: create_point_from_affine
and deserialize
. Both are checking the defining equation already.
But the attribute of the projective point is set to public. So probably one could instantiate a mutable point from outside this crate and later change its coordinates. That would brake the "is on curve" invariant. That's a problem.
You can assume in this PR that any instance of type ShortWeierstrassProjectivePoint<E>
is a point that's on the curve E
. Let's discuss the other issue somewhere else. Feel free to create an issue for it (if you can provide an example and a candidate solution, even better!).
pub fn is_on_curve(point: &<Self as IsEllipticCurve>::PointRepresentation) -> bool { | ||
let [x, y, z] = point.coordinates(); | ||
|
||
let lhs = { | ||
let y_sq = <Self as IsEllipticCurve>::BaseField::square(y.value()); | ||
<Self as IsEllipticCurve>::BaseField::mul(&y_sq, z.value()) | ||
}; | ||
|
||
let rhs = { | ||
let x_sq = <Self as IsEllipticCurve>::BaseField::square(x.value()); | ||
let x_cubed = <Self as IsEllipticCurve>::BaseField::mul(&x_sq, x.value()); | ||
|
||
let z_sq = <Self as IsEllipticCurve>::BaseField::square(z.value()); | ||
let z_cubed = <Self as IsEllipticCurve>::BaseField::mul(&z_sq, z.value()); | ||
|
||
let z_cubed_with_b = | ||
<Self as IsEllipticCurve>::BaseField::mul(&z_cubed, Self::b().value()); | ||
|
||
<Self as IsEllipticCurve>::BaseField::add(&x_cubed, &z_cubed_with_b) | ||
}; | ||
|
||
lhs == rhs | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something along these lines should do the work. Slower performancewise probably. But I wouldn't worry about that, since we didn't tackle optimizations of elliptic curve operations yet.
pub fn is_on_curve(point: &<Self as IsEllipticCurve>::PointRepresentation) -> bool { | |
let [x, y, z] = point.coordinates(); | |
let lhs = { | |
let y_sq = <Self as IsEllipticCurve>::BaseField::square(y.value()); | |
<Self as IsEllipticCurve>::BaseField::mul(&y_sq, z.value()) | |
}; | |
let rhs = { | |
let x_sq = <Self as IsEllipticCurve>::BaseField::square(x.value()); | |
let x_cubed = <Self as IsEllipticCurve>::BaseField::mul(&x_sq, x.value()); | |
let z_sq = <Self as IsEllipticCurve>::BaseField::square(z.value()); | |
let z_cubed = <Self as IsEllipticCurve>::BaseField::mul(&z_sq, z.value()); | |
let z_cubed_with_b = | |
<Self as IsEllipticCurve>::BaseField::mul(&z_cubed, Self::b().value()); | |
<Self as IsEllipticCurve>::BaseField::add(&x_cubed, &z_cubed_with_b) | |
}; | |
lhs == rhs | |
} | |
} | |
pub fn is_on_curve(point: &<Self as IsEllipticCurve>::PointRepresentation) -> bool { | |
if point.z() == &FieldElement::zero() { | |
true | |
} else { | |
let point = point.to_affine(); | |
Self::defining_equation(&point.x(), &point.y()) == FieldElement::zero() | |
} | |
} |
On the other hand. I saw that you use this method on a point that's already of type ShortWeierstrassProjectivePoint<BLS12381TwistCurve>
. That shouldn't be necessary. If the current state of the code allows one to instantiate an element of that type that's not on the curve, then that's a bigger problem. If you suspect that's the case, please feel free to create an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain it more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ShortWeierstrassProjectivePoint<BLS12381TwistCurve>
is supposed to model a point on the twist. If there is an instance of it for which the defining equation of the twist does not hold, then there's a constructor failing to raise a creation error.
fn conjugate_works() { | ||
let a: QuadraticExtensionFieldElement<MyQuadraticNonResidue> = | ||
QuadraticExtensionFieldElement::zero(); | ||
let mut expected = a.conjugate(); | ||
expected = expected.conjugate(); | ||
|
||
assert_eq!(a, expected); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is not testing that conjugate is properly implemented. It only tests that its square is the identity, which is also true for the identity function. I mean. conjugate
could be implemented as the identity and this test would pass.
But besides that. Why are you adding this test? This is functionality of generic quadratic extension fields and its already tested here.
Please remove it and also remove MyQuadraticNonResidue
that's only used for this I guess.
#[derive(Debug, Clone)] | ||
pub struct MyQuadraticNonResidue; | ||
|
||
impl HasQuadraticNonResidue for MyQuadraticNonResidue { | ||
type BaseField = BLS12381PrimeField; | ||
|
||
fn residue() -> FieldElement<Self::BaseField> { | ||
-FieldElement::one() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code for testing purposes should go in the tests
module
#[derive(Debug, Clone)] | |
pub struct MyQuadraticNonResidue; | |
impl HasQuadraticNonResidue for MyQuadraticNonResidue { | |
type BaseField = BLS12381PrimeField; | |
fn residue() -> FieldElement<Self::BaseField> { | |
-FieldElement::one() | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see now that you were using it for the conjugate function. Not only for testing
@@ -109,8 +95,7 @@ impl ShortWeierstrassProjectivePoint<BLS12381TwistCurve> { | |||
let seed_times_p = self.operate_with_self(SEED); | |||
let psi_plus_seed_times_p = self.psi().operate_with(&seed_times_p); | |||
|
|||
Degree2ExtensionField::is_zero(psi_plus_seed_times_p.z().value()) | |||
&& BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) | |||
psi_plus_seed_times_p.z() == &FieldElement::zero() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Short Weierstrass points have a is_neutral_element
already implemented with this logic. You can use that here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, thanks for incorporating all the previous changes!
I found an error on the subgroup check for G2. You prolly removed the wrong line in the previous version. Anyway, I left a few requests that aim to catch that kind of mistakes.
@@ -112,13 +104,13 @@ mod tests { | |||
fn test_zero_point() { | |||
let g1 = BLS12381Curve::generator(); | |||
|
|||
assert!(super::check_point_is_in_subgroup(&g1)); | |||
assert!(g1.is_in_subgroup()); | |||
let new_x = BLS12381FieldElement::zero(); | |||
let new_y = BLS12381FieldElement::one() + BLS12381FieldElement::one(); | |||
|
|||
let false_point2 = G1Point::from_affine(new_x, new_y).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename the variable. There's no such thing as a false point. Add please a test that asserts that the point is not in the subgroup by other means. For example asserting that it's order does not divide the order of the subgroup being checked.
let false_point2 = G1Point::from_affine(new_x, new_y).unwrap(); | |
let point_not_in_subgroup = G1Point::from_affine(new_x, new_y).unwrap(); |
@@ -112,13 +104,13 @@ mod tests { | |||
fn test_zero_point() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename this test
fn test_zero_point() { | |
fn test_point_not_in_subgroup() { |
let seed_times_p = self.operate_with_self(SEED); | ||
let psi_plus_seed_times_p = self.psi().operate_with(&seed_times_p); | ||
|
||
BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not checking that
BLS12381TwistCurve::is_on_curve(&psi_plus_seed_times_p) | |
psi_plus_seed_times_p.is_neutral_element() |
#[test] | ||
fn check_generator_g2_in_subgroup() { | ||
let gen = BLS12381TwistCurve::generator(); | ||
assert!(gen.is_in_subgroup()); | ||
} | ||
|
||
#[test] | ||
fn check_arbitrary_g2_point_in_subgroup() { | ||
let arb_point = BLS12381TwistCurve::generator().operate_with_self(420_u32); | ||
assert!(arb_point.is_in_subgroup()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a test that checks that is_in_subgroup
returns false for a point not in
impl ShortWeierstrassProjectivePoint<BLS12381TwistCurve> { | ||
/// Returns the "Untwist-Frobenius-Twist" endomorphism | ||
pub fn psi(&self) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a test that checks that this endomorphism has minimal polynomial
This is stale |
Optimized subgroup checks using Endomorphism
Description
Existing subgroup check involves mutliplying a point$P$ by $r$ where $r$ is the prime order subgroup. This PR improves the performance of subgroup checks for $G_1$ and $G_2$ by using endomorphism. Theoritically, we will have 2x speedup for $G_1$ and 4x speedup for $G_2$ .
#559 implements Jacobian coordinates just for subgroup check which is not necessary.
Type of change
Checklist