Skip to content

Commit

Permalink
feat: implement sswu hash-to-curve for secp256k1
Browse files Browse the repository at this point in the history
  • Loading branch information
han0110 committed May 30, 2023
1 parent 8b8f22b commit c6b96bf
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 1 deletion.
117 changes: 117 additions & 0 deletions src/hash_to_curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,120 @@ where
r
})
}

/// Implements a degree 3 isogeny map.
/// Copy from https://github.com/zcash/pasta_curves/blob/7e3fc6a4919f6462a32b79dd226cb2587b7961eb/src/hashtocurve.rs#L81
fn degree_3_iso_map<C: CurveExt>(p: &C, iso: &[C::Base; 13]) -> C {
// The input and output are in Jacobian coordinates, using the method
// in "Avoiding inversions" [WB2019, section 4.3].

let (x, y, z) = p.jacobian_coordinates();

let z2 = z.square();
let z3 = z2 * z;
let z4 = z2.square();
let z6 = z3.square();

let num_x = ((iso[0] * x + iso[1] * z2) * x + iso[2] * z4) * x + iso[3] * z6;
let div_x = (z2 * x + iso[4] * z4) * x + iso[5] * z6;

let num_y = (((iso[6] * x + iso[7] * z2) * x + iso[8] * z4) * x + iso[9] * z6) * y;
let div_y = (((x + iso[10] * z2) * x + iso[11] * z4) * x + iso[12] * z6) * z3;

let zo = div_x * div_y;
let xo = num_x * div_y * zo;
let yo = num_y * div_x * zo.square();

C::new_jacobian(xo, yo, zo).unwrap()
}

/// Implementation of https://www.ietf.org/id/draft-irtf-cfrg-hash-to-curve-16.html#name-simplified-swu-for-ab-0 with degree 3 isogeny.
/// Modified from https://github.com/zcash/pasta_curves/blob/7e3fc6a4919f6462a32b79dd226cb2587b7961eb/src/hashtocurve.rs#L109
#[allow(clippy::type_complexity)]
pub(crate) fn degree_3_sswu_map_to_curve<'a, C>(
curve_id: &'static str,
domain_prefix: &'a str,
z: C::Base,
theta: C::Base,
isogeny_constants: [C::Base; 13],
) -> Box<dyn Fn(&[u8]) -> C + 'a>
where
C: CurveExt,
C::Base: FromUniformBytes<64>,
{
Box::new(move |message| {
let mut us = [C::Base::ZERO; 2];
hash_to_field("SSWU", curve_id, domain_prefix, message, &mut us);

let [q0, q1] = us.map(|u| {
// 1. tv1 = inv0(Z^2 * u^4 + Z * u^2)
// 2. x1 = (-B / A) * (1 + tv1)
// 3. If tv1 == 0, set x1 = B / (Z * A)
// 4. gx1 = x1^3 + A * x1 + B
//
// We use the "Avoiding inversions" optimization in [WB2019, section 4.2]
// (not to be confused with section 4.3):
//
// here [WB2019]
// ------- ---------------------------------
// Z ξ
// u t
// Z * u^2 ξ * t^2 (called u, confusingly)
// x1 X_0(t)
// x2 X_1(t)
// gx1 g(X_0(t))
// gx2 g(X_1(t))
//
// Using the "here" names:
// x1 = num_x1/div = [B*(Z^2 * u^4 + Z * u^2 + 1)] / [-A*(Z^2 * u^4 + Z * u^2]
// gx1 = num_gx1/div_gx1 = [num_x1^3 + A * num_x1 * div^2 + B * div^3] / div^3

let a = C::a();
let b = C::b();
let z_u2 = z * u.square();
let ta = z_u2.square() + z_u2;
let num_x1 = b * (ta + C::Base::ONE);
let div = a * C::Base::conditional_select(&-ta, &z, ta.is_zero());
let num2_x1 = num_x1.square();
let div2 = div.square();
let div3 = div2 * div;
let num_gx1 = (num2_x1 + a * div2) * num_x1 + b * div3;

// 5. x2 = Z * u^2 * x1
let num_x2 = z_u2 * num_x1; // same div

// 6. gx2 = x2^3 + A * x2 + B [optimized out; see below]
// 7. If is_square(gx1), set x = x1 and y = sqrt(gx1)
// 8. Else set x = x2 and y = sqrt(gx2)
let (gx1_square, y1) = C::Base::sqrt_ratio(&num_gx1, &div3);

// This magic also comes from a generalization of [WB2019, section 4.2].
//
// The Sarkar square root algorithm with input s gives us a square root of
// h * s for free when s is not square, where h is a fixed nonsquare.
// In our implementation, h = ROOT_OF_UNITY.
// We know that Z / h is a square since both Z and h are
// nonsquares. Precompute theta as a square root of Z / ROOT_OF_UNITY.
//
// We have gx2 = g(Z * u^2 * x1) = Z^3 * u^6 * gx1
// = (Z * u^3)^2 * (Z/h * h * gx1)
// = (Z * theta * u^3)^2 * (h * gx1)
//
// When gx1 is not square, y1 is a square root of h * gx1, and so Z * theta * u^3 * y1
// is a square root of gx2. Note that we don't actually need to compute gx2.

let y2 = theta * z_u2 * u * y1;
let num_x = C::Base::conditional_select(&num_x2, &num_x1, gx1_square);
let y = C::Base::conditional_select(&y2, &y1, gx1_square);

// 9. If sgn0(u) != sgn0(y), set y = -y
let y = C::Base::conditional_select(&(-y), &y, u.is_odd().ct_eq(&y.is_odd()));

C::new_jacobian(num_x * div, y * div3, div).unwrap()
});

let r = q0 + &q1;
debug_assert!(bool::from(r.is_on_curve()));
degree_3_iso_map(&r, &isogeny_constants)
})
}
116 changes: 115 additions & 1 deletion src/secp256k1/curve.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::ff::WithSmallOrderMulGroup;
use crate::ff::{Field, PrimeField};
use crate::group::{prime::PrimeCurveAffine, Curve, Group as _, GroupEncoding};
use crate::hash_to_curve::degree_3_sswu_map_to_curve;
use crate::secp256k1::Fp;
use crate::secp256k1::Fq;
use crate::{Coordinates, CurveAffine, CurveAffineExt, CurveExt};
Expand Down Expand Up @@ -61,9 +62,110 @@ new_curve_impl!(
(SECP_GENERATOR_X,SECP_GENERATOR_Y),
SECP_B,
"secp256k1",
|_, _| unimplemented!(),
|curve_id, domain_prefix| degree_3_sswu_map_to_curve(curve_id, domain_prefix, Secp256k1::Z, Secp256k1::THETA, Secp256k1::ISOGENY_CONSTANTS),
);

impl Secp256k1 {
/// Constants used for computing the degree 3 isogeny map.
/// Taken from https://www.ietf.org/id/draft-irtf-cfrg-hash-to-curve-16.html#appx-iso-secp256k1.
pub const ISOGENY_CONSTANTS: [Fp; 13] = [
Fp::from_raw([
0x8e38e38daaaaa88c,
0x38e38e38e38e38e3,
0xe38e38e38e38e38e,
0x8e38e38e38e38e38,
]),
Fp::from_raw([
0x4ecbd0b53d9dd262,
0xe4506144037c4031,
0xe2a413deca25caec,
0x534c328d23f234e6,
]),
Fp::from_raw([
0xdfff1044f17c6581,
0xd595d2fc0bf63b92,
0xb9f315cea7fd44c5,
0x07d3d4c80bc321d5,
]),
Fp::from_raw([
0x8e38e38daaaaa8c7,
0x38e38e38e38e38e3,
0xe38e38e38e38e38e,
0x8e38e38e38e38e38,
]),
Fp::from_raw([
0xc52a56612a8c6d14,
0x06d36b641f5e41bb,
0xf7c4b2d51b542254,
0xedadc6f64383dc1d,
]),
Fp::from_raw([
0x9fe6b745781eb49b,
0x86cd409542f8487d,
0x9ca34ccbb7b640dd,
0xd35771193d94918a,
]),
Fp::from_raw([
0x84bda12f38e38d84,
0xbda12f684bda12f6,
0xa12f684bda12f684,
0x2f684bda12f684bd,
]),
Fp::from_raw([
0xa765e85a9ecee931,
0x722830a201be2018,
0x715209ef6512e576,
0x29a6194691f91a73,
]),
Fp::from_raw([
0xdffc90fc201d71a3,
0x647ab046d686da6f,
0xa9d0a54b12a0a6d5,
0xc75e0c32d5cb7c0f,
]),
Fp::from_raw([
0xa12f684b8e38e23c,
0x2f684bda12f684bd,
0x684bda12f684bda1,
0x4bda12f684bda12f,
]),
Fp::from_raw([
0xa7bf8192bfd2a76f,
0x0a3d21162f0d6299,
0xf3a70c3fa8fe337e,
0x6484aa716545ca2c,
]),
Fp::from_raw([
0xdfb425d2685c2573,
0x9467c1bfc8e8d978,
0xd5e9e6632722c298,
0x7a06534bb8bdb49f,
]),
Fp::from_raw([
0xfffffffefffff93b,
0xffffffffffffffff,
0xffffffffffffffff,
0xffffffffffffffff,
]),
];

/// Z = -11
pub const Z: Fp = Fp::from_raw([
0xfffffffefffffc24,
0xffffffffffffffff,
0xffffffffffffffff,
0xffffffffffffffff,
]);

/// `(F::ROOT_OF_UNITY.invert().unwrap() * z).sqrt().unwrap()`
pub const THETA: Fp = Fp::from_raw([
0x286729c8303c4a59,
0xec184f00a74789dd,
0x7ad13fb38f842afe,
0x31fdf302724013e5,
]);
}

impl CurveAffineExt for Secp256k1Affine {
batch_add!();

Expand All @@ -72,6 +174,18 @@ impl CurveAffineExt for Secp256k1Affine {
}
}

#[test]
fn test_hash_to_curve() {
assert_eq!(Fp::ZERO - Fp::from(11), Secp256k1::Z);
assert_eq!(
(Fp::ROOT_OF_UNITY.invert().unwrap() * Secp256k1::Z)
.sqrt()
.unwrap(),
Secp256k1::THETA,
);
crate::tests::curve::hash_to_curve_test::<Secp256k1>();
}

#[test]
fn test_curve() {
crate::tests::curve::curve_tests::<Secp256k1>();
Expand Down

0 comments on commit c6b96bf

Please sign in to comment.