Skip to content

ec: implement double-odd curves #986

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 21 commits into
base: master
Choose a base branch
from

Conversation

rubdos
Copy link
Contributor

@rubdos rubdos commented May 19, 2025

Implements Thomas Pornin's “A Prime-Order Group with Complete Formulas from Even-Order Elliptic Curves” [1], often referred to as "double odd" curves.

Includes the double-odd curve “JQ255s” as presented in the paper.

[1] T. Pornin, “A Prime-Order Group with Complete Formulas from Even-Order Elliptic Curves,” IACR CiC, vol. 1, no. 1, p. 33, Apr. 2024, doi: 10.62056/akmp-4c2h.

This is work by @Tarinn

Description

closes: #490


Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • Targeted PR against correct branch (master)
  • Linked to GitHub issue with discussion and accepted design OR have an explanation in the PR that describes this work.
  • Wrote unit tests
  • Updated relevant documentation in the code
  • Added a relevant changelog entry to the Pending section in CHANGELOG.md
  • Re-reviewed Files changed in the GitHub PR explorer

@rubdos rubdos requested review from a team as code owners May 19, 2025 11:30
@rubdos rubdos requested review from Pratyush, mmagician and weikengchen and removed request for a team May 19, 2025 11:30
@rubdos
Copy link
Contributor Author

rubdos commented May 19, 2025

As a side note: I recall a discussion on Discord where there was a parameter dump for a DO curve that embeds Curve25519, but I don't have the link at hand. Here are the SW parameters:

a = 57259562424833564022627223828891588493930540394420821089419290001039534118212
b = 64131260960803756922173133202372554928955727810351288169387335111902918457126
|F_q| = p = 115792089237316195423570985008687907853682756971699735333147980285963064639179

Copy link
Member

@Pratyush Pratyush left a 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! Left some comments!

@rubdos
Copy link
Contributor Author

rubdos commented May 23, 2025

Pushed a few fixups, will discuss the rest with @Tarinn when we both have time at the office! Thanks for the review so far.

@rubdos rubdos force-pushed the double-odd-public branch 4 times, most recently from 19b1822 to e084df6 Compare May 28, 2025 12:51
Robrecht and others added 2 commits May 28, 2025 15:34
Implements Thomas Pornin's “A Prime-Order Group with Complete Formulas
from Even-Order Elliptic Curves” [1], often referred to as "double odd"
curves.

Includes the double-odd curve “JQ255s” as presented in the paper.

[1] T. Pornin, “A Prime-Order Group with Complete Formulas from Even-Order Elliptic Curves,” IACR CiC, vol. 1, no. 1, p. 33, Apr. 2024, doi: 10.62056/akmp-4c2h.
@rubdos rubdos force-pushed the double-odd-public branch from e084df6 to 7564ed8 Compare May 28, 2025 13:34
@rubdos rubdos requested a review from Pratyush May 28, 2025 13:52
@rubdos
Copy link
Contributor Author

rubdos commented Jun 20, 2025

@Pratyush, @samvandevelde noticed that the current implementation was tailored to Jq255s, where A=-1. This has meanwhile been resolved (766b91a). He'll also take care of your further comments. Thanks for the feedback again, as always!

use crate::{constraints::FqVar, *};
use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar;

/// A group element in the Curve25519 curve.
Copy link
Member

Choose a reason for hiding this comment

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

Please update this comment.

Choose a reason for hiding this comment

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

Dear Prof. Mishra,

This has been dealt with in commit 572b797

Kind regards,
Sam

}

#[test]
fn valid_decode() {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a source for these test vectors? If so could you please add a link here?

Choose a reason for hiding this comment

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

Dear Prof. Mishra,

This has been added in commit 572b797

Kind regards,
Sam

Comment on lines +72 to +77
pub fn n() -> Self {
Self {
e: -P::BaseField::ONE,
u: P::BaseField::ZERO,
}
}
Copy link
Member

Choose a reason for hiding this comment

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

What is the point represented by this function?

Choose a reason for hiding this comment

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

Dear Prof. Mishra,

More explanation about N has been provided in commit 572b797

Kind regards,
Sam

Comment on lines 91 to 99
// Compute the curve equation x(x^2 + Ax + B).
let e = (P::get_c() * u.square().square() - (P::COEFF_A * u.square()).double()
+ P::BaseField::ONE)
.sqrt()?;
let neg_e = -e;
match e < neg_e {
true => Some((e, neg_e)),
false => Some((neg_e, e)),
}
Copy link
Member

Choose a reason for hiding this comment

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

How is this value for e derived? It doesn't seem to match the curve equation?

Choose a reason for hiding this comment

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

Dear Prof. Mishra,
The correct (Jacobi Quartic) equation has been provided in commit 572b797

Kind regards,
Sam

Comment on lines +104 to +106
let e_squared = P::get_c() * self.u.square().square()
- (P::COEFF_A * self.u.square()).double()
+ P::BaseField::ONE;
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

Choose a reason for hiding this comment

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

Dear Prof. Mishra,
The correct (Jacobi Quartic) equation has been provided in commit 572b797

Kind regards,
Sam

}

impl<P: DOCurveConfig, T: Borrow<Affine<P>>> AddAssign<T> for Projective<P> {
fn add_assign(&mut self, other: T) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a reference for this code? Perhaps a pointer to existing "projective + projective" formulae, along with a description of the simplification when one of the points is affine.

Choose a reason for hiding this comment

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

Dear Prof. Mishra,
The missing reference to the addition formulae has been provided in commit 572b797

Kind regards,
Sam

Comment on lines +311 to +336
if self.is_zero() {
*self = *other;
return;
}

if other.is_zero() {
return;
}

let n1 = self.e * other.e;
let n2 = self.z * other.z;
let n3 = self.u * other.u;
let n4 = self.t * other.t;

let n5 = (self.z + self.t) * (other.z + other.t) - n2 - n4;
self.t = (self.e + self.u) * (other.e + other.u) - n1 - n3;
let c = P::get_c();
let cn4 = c * n4;
self.z = n2 - cn4;

let n3d = n3.double();

self.e = (n2 + cn4) * (n1 - P::COEFF_A * n3d) + c * n3d * n5;
self.u = self.z * self.t;
self.z.square_in_place();
self.t.square_in_place();
Copy link
Member

Choose a reason for hiding this comment

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

Same here; please add a reference.

Choose a reason for hiding this comment

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

Dear Prof. Mishra,
The missing reference to the addition formulae has been provided in commit 572b797

Kind regards,
Sam

Comment on lines +70 to +85
match compress {
Compress::Yes => {
let mut buffer = ark_std::vec::Vec::new();
item.e.serialize_uncompressed(&mut buffer)?;
if buffer[0] & 1u8 == 1u8 {
-item.u
} else {
item.u
}
.serialize_uncompressed(writer)
},
Compress::No => {
item.e.serialize_with_mode(&mut writer, compress)?;
item.u.serialize_with_mode(&mut writer, compress)
},
}
Copy link
Member

Choose a reason for hiding this comment

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

Could you provide a reference as to how this matches up with the official encoding described in Section 6.1.1 of https://doubleodd.group/doubleodd.pdf?

Choose a reason for hiding this comment

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

Dear Prof. Mishra,

The referenced official encoding was for do255e and do255s only (and rephrased in the follow-up paper for jq255e/s, https://doubleodd.group/doubleodd-jq.pdf page 23). However, was meant to standardize encoding for these specific curves, so it seemed too specific to add in the comments of the model for all double odd curves.

That being said, I compared the specifications with the arkworks implementation of these operations for Fp,
and Fp matches up with the described encoding (see below).

Should these results perhaps be writen in the comments of algebra/curves/jq255s instead?

Kind regards,
Sam


  1. A field element x ∈ Fq is considered as an integer in the 0 to q − 1 range, which is then encoded using the unsigned little-endian convention (least significant byte comes first).

OK: Serialization of Fp uses copy_from_u64_slice(), which uses from_le_bytes.

  1. Encoding always uses 32 bytes, even if the integer could have fit into fewer bytes

OK: Without any flags, 32 bytes are used for the q and r described in the paper.

  1. Decoders MUST reject the source bytes, and return Invalid, if any of the following conditions hold:
    • The source length is not exactly 32 bytes.
    • The integer resulting from unsigned little-endian interpretation of the source bytes is not in the 0 to q − 1 range.
    In particular, there is no ignored bit: even though the value of q implies that the most significant bit of the last byte is always zero, that bit MUST NOT be ignored by decoders. Moreover, checking that the most significant bit of the last byte is zero is not sufficient validation; for instance, an encoding of the integer q itself, over 32 bytes, MUST be rejected as Invalid, since it is not in the 0 to q − 1 range. There is no implicit reduction modulo q.

OK: Deserialization to Fp uses from_bigint(), which returns an Option depending on is_geq_modulus(), as well as from_le_bytes(), over the entirety of the 32 bytes (fixed to 32 by read_exact_up_to(...,output_byte_size))

Comment on lines 96 to 101
let e: Self::BaseField = (Self::get_c() * u.square().square()
- (Self::COEFF_A * u.square()).double()
+ Self::BaseField::ONE)
.sqrt()
.ok_or(SerializationError::InvalidData)
.unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

Could this be deduplicated by calling the get_e_from_u_unchecked fn on Affine?

Choose a reason for hiding this comment

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

Dear Prof. Mishra,
The missing reference to the addition formulae has been provided in commit f75be11

Kind regards,
Sam

samvandevelde and others added 4 commits July 22, 2025 10:00
Although the same value, this rewrite more clearly indicates the value of COEFF_A (equal to -1 as described in https://eprint.iacr.org/2022/1052)

Co-authored-by: Pratyush Mishra <pratyush795@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement double-odd curves
4 participants