Skip to content

feat: twisted edwards curves #633

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/crypto/src/curve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::{

mod helpers;
pub mod sw;
pub mod te;

/// Elliptic curves can be represented via different "models" with varying
/// efficiency properties.
Expand Down
2 changes: 1 addition & 1 deletion lib/crypto/src/curve/sw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pub trait SWCurveConfig: super::CurveConfig {
}

/// Default implementation of group multiplication for projective
/// coordinates
/// coordinates.
fn mul_projective(
base: &Projective<Self>,
scalar: impl BitIteratorBE,
Expand Down
2 changes: 1 addition & 1 deletion lib/crypto/src/curve/sw/projective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ impl<P: SWCurveConfig> CurveGroup for Projective<P> {

batch_inversion(&mut z_s);

// Perform affine transformations
// Perform affine transformations.
v.iter()
.zip(z_s)
.map(|(g, z)| {
Expand Down
250 changes: 250 additions & 0 deletions lib/crypto/src/curve/te/affine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
//! Affine coordinates for a point on a Twisted Edwards curve ([Affine Space]).
//!
//! [Affine Space]: https://en.wikipedia.org/wiki/Affine_space
use core::{
borrow::Borrow,
fmt::{Debug, Display, Formatter},
ops::{Add, Mul, Neg, Sub},
};

use educe::Educe;
use num_traits::{One, Zero};
use zeroize::Zeroize;

use super::{Projective, TECurveConfig};
use crate::{
bits::BitIteratorBE,
curve::AffineRepr,
field::{group::AdditiveGroup, prime::PrimeField, Field},
};

/// Affine coordinates for a point on a twisted Edwards curve, over the
/// base field `P::BaseField`.
#[derive(Educe)]
#[educe(Copy, Clone, PartialEq, Eq, Hash)]
#[must_use]
pub struct Affine<P: TECurveConfig> {
/// X coordinate of the point represented as a field element
pub x: P::BaseField,
/// Y coordinate of the point represented as a field element
pub y: P::BaseField,
}

impl<P: TECurveConfig> Display for Affine<P> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
if self.is_zero() {
write!(f, "infinity")
} else {
write!(f, "({}, {})", self.x, self.y)
}
}
}

impl<P: TECurveConfig> Debug for Affine<P> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
if self.is_zero() {
write!(f, "infinity")
} else {
write!(f, "({}, {})", self.x, self.y)
}
}
}

impl<P: TECurveConfig> PartialEq<Projective<P>> for Affine<P> {
fn eq(&self, other: &Projective<P>) -> bool {
&self.into_group() == other
}
}

impl<P: TECurveConfig> Affine<P> {
/// Construct a new group element without checking whether the coordinates
/// specify a point in the subgroup.
pub const fn new_unchecked(x: P::BaseField, y: P::BaseField) -> Self {
Self { x, y }
}

/// Construct a new group element in a way while enforcing that points are
/// in the prime-order subgroup.
///
/// # Panics
///
/// * If point is not on curve.
/// * If point is not in the prime-order subgroup.
pub fn new(x: P::BaseField, y: P::BaseField) -> Self {
Copy link
Collaborator

@0xNeshi 0xNeshi Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some general comments:

  • could any of the new functions be marked with inline(always) or must_use?
  • some flows are not covered with unit tests
  • could we implement any proptests for this?
  • missing CHANGELOG

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @0xNeshi 💯

let p = Self::new_unchecked(x, y);
assert!(p.is_on_curve());
assert!(p.is_in_correct_subgroup_assuming_on_curve());
p
}

/// Construct the identity of the group.
pub const fn zero() -> Self {
Self::new_unchecked(P::BaseField::ZERO, P::BaseField::ONE)
}

/// Is this point the identity?
pub fn is_zero(&self) -> bool {
self.x.is_zero() && self.y.is_one()
}

/// Checks that the current point is on the elliptic curve.
pub fn is_on_curve(&self) -> bool {
let x2 = self.x.square();
let y2 = self.y.square();

let lhs = y2 + P::mul_by_a(x2);
let rhs = P::BaseField::one() + (P::COEFF_D * (x2 * y2));

lhs == rhs
}
}

impl<P: TECurveConfig> Affine<P> {
/// Checks if this point is in the prime-order subgroup.
///
/// This assumes the point is already on the curve and verifies it belongs
/// to the subgroup with order equal to `P::ScalarField`.
pub fn is_in_correct_subgroup_assuming_on_curve(&self) -> bool {
P::is_in_correct_subgroup_assuming_on_curve(self)
}
}

impl<P: TECurveConfig> AffineRepr for Affine<P> {
type BaseField = P::BaseField;
type Config = P;
type Group = Projective<P>;
type ScalarField = P::ScalarField;

fn xy(&self) -> Option<(Self::BaseField, Self::BaseField)> {
(!self.is_zero()).then_some((self.x, self.y))
}
Comment on lines +118 to +120
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think it's better to return None for zero than to return actual zero tuple ((P::BaseField::ZERO, P::BaseField::ONE))?


fn generator() -> Self {
P::GENERATOR
}

fn zero() -> Self {
Self::new_unchecked(P::BaseField::ZERO, P::BaseField::ONE)
}

fn mul_bigint(&self, by: impl BitIteratorBE) -> Self::Group {
P::mul_affine(self, by)
}

/// Multiplies this element by the cofactor and output the
/// resulting projective element.
fn mul_by_cofactor_to_group(&self) -> Self::Group {
P::mul_affine(self, Self::Config::COFACTOR)
}

/// Performs cofactor clearing.
/// The default method is simply to multiply by the cofactor.
/// Some curves can implement a more efficient algorithm.
fn clear_cofactor(&self) -> Self {
P::clear_cofactor(self)
}
}

impl<P: TECurveConfig> Zeroize for Affine<P> {
// The phantom data does not contain element-specific data
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which "phantom data" is this referring to?

// and thus does not need to be zeroized.
fn zeroize(&mut self) {
self.x.zeroize();
self.y.zeroize();
}
Comment on lines +151 to +154
Copy link
Collaborator

@0xNeshi 0xNeshi Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Affine::<P>::zero() returns:

        Self::new_unchecked(P::BaseField::ZERO, P::BaseField::ONE) // (x: 0, y: 1)

Shouldn't this be something like:

fn zeroize(&mut self) {
    self.x.zeroize();
    self.y = P::BaseField::ONE;
}

?

}

impl<P: TECurveConfig> Neg for Affine<P> {
type Output = Self;

fn neg(self) -> Self {
Self::new_unchecked(-self.x, self.y)
}
}

impl<P: TECurveConfig, T: Borrow<Self>> Add<T> for Affine<P> {
type Output = Projective<P>;

fn add(self, other: T) -> Self::Output {
let mut copy = self.into_group();
copy += other.borrow();
copy
}
}

impl<P: TECurveConfig> Add<Projective<P>> for Affine<P> {
type Output = Projective<P>;

fn add(self, other: Projective<P>) -> Projective<P> {
other + self
}
}

impl<'a, P: TECurveConfig> Add<&'a Projective<P>> for Affine<P> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current Add and Sub impls allow:

  1. owned owned
  2. owned reference

Would be useful to have impls for:

  1. reference reference

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be implemented with impl_additive_ops_from_ref!?

type Output = Projective<P>;

fn add(self, other: &'a Projective<P>) -> Projective<P> {
*other + self
}
}

impl<P: TECurveConfig, T: Borrow<Self>> Sub<T> for Affine<P> {
type Output = Projective<P>;

fn sub(self, other: T) -> Self::Output {
let mut copy = self.into_group();
copy -= other.borrow();
copy
}
}

impl<P: TECurveConfig> Sub<Projective<P>> for Affine<P> {
type Output = Projective<P>;

fn sub(self, other: Projective<P>) -> Projective<P> {
self + (-other)
}
}

impl<'a, P: TECurveConfig> Sub<&'a Projective<P>> for Affine<P> {
type Output = Projective<P>;

fn sub(self, other: &'a Projective<P>) -> Projective<P> {
self + (-*other)
}
}

impl<P: TECurveConfig> Default for Affine<P> {
#[inline]
fn default() -> Self {
Self::zero()
}
}

impl<P: TECurveConfig, T: Borrow<P::ScalarField>> Mul<T> for Affine<P> {
type Output = Projective<P>;

#[inline]
fn mul(self, other: T) -> Self::Output {
self.mul_bigint(other.borrow().into_bigint())
}
}

// The projective point X, Y, T, Z is represented in the affine
// coordinates as X/Z, Y/Z.
impl<P: TECurveConfig> From<Projective<P>> for Affine<P> {
fn from(p: Projective<P>) -> Affine<P> {
if p.is_zero() {
Affine::zero()
} else if p.z.is_one() {
// If Z is one, the point is already normalized.
Affine::new_unchecked(p.x, p.y)
} else {
// Z is nonzero, so it must have inverse in a field.
let z_inv = p.z.inverse().unwrap();
let x = p.x * z_inv;
let y = p.y * z_inv;
Affine::new_unchecked(x, y)
}
}
}
Loading
Loading