diff --git a/.gitignore b/.gitignore index 4d9d603f74..322e683827 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ internal/tests/integration/ field/internal/dev.go field/internal/dev/** field/generator/addchain/** +internal/generator/field/addchain/ +/addchain/ .vscode diff --git a/ecc/bls12-377/twistededwards/curve.go b/ecc/bls12-377/twistededwards/curve.go index 45f8bfb5da..928591a00e 100644 --- a/ecc/bls12-377/twistededwards/curve.go +++ b/ecc/bls12-377/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bls12-377/twistededwards/point_test.go b/ecc/bls12-377/twistededwards/point_test.go index ac9825a620..45df2abce7 100644 --- a/ecc/bls12-377/twistededwards/point_test.go +++ b/ecc/bls12-377/twistededwards/point_test.go @@ -823,6 +823,106 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1176,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bls12-377/twistededwards/subgroup.go b/ecc/bls12-377/twistededwards/subgroup.go new file mode 100644 index 0000000000..ed225ae617 --- /dev/null +++ b/ecc/bls12-377/twistededwards/subgroup.go @@ -0,0 +1,359 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByQuarticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _101 = _10 + _11 + // _110 = 1 + _101 + // _1000 = _10 + _110 + // _10000 = 2*_1000 + // _10110 = _110 + _10000 + // _100000 = 2*_10000 + // _100011 = _11 + _100000 + // _101011 = _1000 + _100011 + // _101101 = _10 + _101011 + // _1011010 = 2*_101101 + // _1011011 = 1 + _1011010 + // _1111011 = _100000 + _1011011 + // _10000101 = _101011 + _1011010 + // _10001011 = _110 + _10000101 + // _10100101 = _100000 + _10000101 + // _10101011 = _110 + _10100101 + // _11000001 = _10110 + _10101011 + // _11000011 = _10 + _11000001 + // _11010001 = _10000 + _11000001 + // _11010011 = _10 + _11010001 + // _11010101 = _10 + _11010011 + // _11100101 = _10000 + _11010101 + // _11101101 = _1000 + _11100101 + // i45 = ((_10000101 + _10100101) << 7 + _1011011) << 10 + _10101011 + // i74 = ((i45 << 8 + _11010011) << 9 + _10001011) << 10 + // i94 = ((_10100101 + i74) << 7 + _101011) << 10 + _11000001 + // i123 = ((i94 << 9 + _11010001) << 10 + _11010001) << 8 + // i142 = ((_11100101 + i123) << 8 + _11000011) << 8 + _1111011 + // i181 = ((i142 << 17 + _101011) << 10 + _11010101) << 10 + // i195 = ((_11101101 + i181) << 8 + _10000 + _11101101) << 3 + // i243 = ((_101 + i195) << 35 + _10000101) << 10 + _100011 + // return i243 << 45 + // + // Operations: 246 squares 42 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16 fr.Element + + t4.Square(&x) + + z.Mul(&x, &t4) + + t1.Mul(&t4, z) + + t8.Mul(&x, &t1) + + t2.Mul(&t4, &t8) + + t3.Square(&t2) + + t7.Mul(&t8, &t3) + + t9.Square(&t3) + + z.Mul(z, &t9) + + t5.Mul(&t2, z) + + t0.Mul(&t4, &t5) + + t0.Square(&t0) + + t15.Mul(&x, &t0) + + t6.Mul(&t9, &t15) + + t0.Mul(&t5, &t0) + + t12.Mul(&t8, &t0) + + t11.Mul(&t9, &t0) + + t14.Mul(&t8, &t11) + + t10.Mul(&t7, &t14) + + t7.Mul(&t4, &t10) + + t9.Mul(&t3, &t10) + + t13.Mul(&t4, &t9) + + t4.Mul(&t4, &t13) + + t8.Mul(&t3, &t4) + + t2.Mul(&t2, &t8) + + t16.Mul(&t0, &t11) + + for range 7 { + t16.Square(&t16) + } + + t15.Mul(&t15, &t16) + + for range 10 { + t15.Square(&t15) + } + + t14.Mul(&t14, &t15) + + for range 8 { + t14.Square(&t14) + } + + t13.Mul(&t13, &t14) + + for range 9 { + t13.Square(&t13) + } + + t12.Mul(&t12, &t13) + + for range 10 { + t12.Square(&t12) + } + + t11.Mul(&t11, &t12) + + for range 7 { + t11.Square(&t11) + } + + t11.Mul(&t5, &t11) + + for range 10 { + t11.Square(&t11) + } + + t10.Mul(&t10, &t11) + + for range 9 { + t10.Square(&t10) + } + + t10.Mul(&t9, &t10) + + for range 10 { + t10.Square(&t10) + } + + t9.Mul(&t9, &t10) + + for range 8 { + t9.Square(&t9) + } + + t8.Mul(&t8, &t9) + + for range 8 { + t8.Square(&t8) + } + + t7.Mul(&t7, &t8) + + for range 8 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 17 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 10 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 10 { + t4.Square(&t4) + } + + t4.Mul(&t2, &t4) + + for range 8 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + t2.Mul(&t2, &t3) + + for range 3 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 35 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + for range 10 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + for range 45 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, v1, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1) + tmp.Square(&v1).Mul(&tmp, &v1) + z.Mul(&z, &tmp) + expByQuarticExp(&check, z) + return check.IsOne() +} diff --git a/ecc/bls12-381/bandersnatch/point_test.go b/ecc/bls12-381/bandersnatch/point_test.go index 4a7cf881a3..8c063e3718 100644 --- a/ecc/bls12-381/bandersnatch/point_test.go +++ b/ecc/bls12-381/bandersnatch/point_test.go @@ -855,6 +855,19 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} + func TestIsInSubGroup(t *testing.T) { t.Parallel() parameters := gopter.DefaultTestParameters() @@ -877,6 +890,14 @@ func TestIsInSubGroup(t *testing.T) { }, )) + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + properties.Property("Test IsInSubGroup", prop.ForAll( func(s big.Int) bool { @@ -890,6 +911,36 @@ func TestIsInSubGroup(t *testing.T) { genS, )) + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + + return true + }, + genS, + )) + properties.TestingRun(t, gopter.ConsoleReporter(false)) } @@ -1153,8 +1204,25 @@ func BenchmarkIsInSubGroup(b *testing.B) { var point PointAffine point.ScalarMultiplication(¶ms.Base, &s) - b.ResetTimer() - for range b.N { - _ = point.IsInSubGroup() + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) } diff --git a/ecc/bls12-381/twistededwards/curve.go b/ecc/bls12-381/twistededwards/curve.go index 308001bef7..0adb551711 100644 --- a/ecc/bls12-381/twistededwards/curve.go +++ b/ecc/bls12-381/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bls12-381/twistededwards/point_test.go b/ecc/bls12-381/twistededwards/point_test.go index 1db80527ed..f100f80bca 100644 --- a/ecc/bls12-381/twistededwards/point_test.go +++ b/ecc/bls12-381/twistededwards/point_test.go @@ -823,6 +823,161 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1231,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bls12-381/twistededwards/subgroup.go b/ecc/bls12-381/twistededwards/subgroup.go new file mode 100644 index 0000000000..3a9fe97927 --- /dev/null +++ b/ecc/bls12-381/twistededwards/subgroup.go @@ -0,0 +1,458 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + initCofactorEightTorsion(&t4) +} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _100 = 1 + _11 + // _110 = _10 + _100 + // _1100 = 2*_110 + // _10010 = _110 + _1100 + // _10011 = 1 + _10010 + // _10110 = _11 + _10011 + // _11000 = _10 + _10110 + // _11010 = _10 + _11000 + // _100010 = _1100 + _10110 + // _110101 = _10011 + _100010 + // _111011 = _110 + _110101 + // _1001011 = _10110 + _110101 + // _1001101 = _10 + _1001011 + // _1010101 = _11010 + _111011 + // _1100111 = _10010 + _1010101 + // _1101001 = _10 + _1100111 + // _10000011 = _11010 + _1101001 + // _10011001 = _10110 + _10000011 + // _10011101 = _100 + _10011001 + // _10111111 = _100010 + _10011101 + // _11010111 = _11000 + _10111111 + // _11011011 = _100 + _11010111 + // _11100111 = _1100 + _11011011 + // _11101111 = _11000 + _11010111 + // _11111111 = _11000 + _11100111 + // i55 = ((_11100111 << 8 + _11011011) << 9 + _10011101) << 9 + // i75 = ((_10011001 + i55) << 9 + _10011001) << 8 + _11010111 + // i102 = ((i75 << 6 + _110101) << 10 + _10000011) << 9 + // i121 = ((_1100111 + i102) << 8 + _111011) << 8 + 1 + // i162 = ((i121 << 14 + _1001101) << 10 + _111011) << 15 + // i183 = ((_1010101 + i162) << 10 + _11101111) << 8 + _1101001 + // i216 = ((i183 << 16 + _10111111) << 8 + _11111111) << 7 + // i236 = ((_1001011 + i216) << 9 + _11111111) << 8 + _10111111 + // i262 = ((i236 << 8 + _11111111) << 8 + _11111111) << 8 + // return ((_11111111 + i262) << 2 + _11) << 29 + // + // Operations: 246 squares 49 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15 fr.Element + + t3.Square(&x) + + z.Mul(&x, &t3) + + t14.Mul(&x, z) + + t2.Mul(&t3, &t14) + + t4.Square(&t2) + + t8.Mul(&t2, &t4) + + t5.Mul(&x, &t8) + + t11.Mul(z, &t5) + + t0.Mul(&t3, &t11) + + t9.Mul(&t3, &t0) + + t1.Mul(&t4, &t11) + + t10.Mul(&t5, &t1) + + t6.Mul(&t2, &t10) + + t2.Mul(&t11, &t10) + + t7.Mul(&t3, &t2) + + t5.Mul(&t9, &t6) + + t8.Mul(&t8, &t5) + + t3.Mul(&t3, &t8) + + t9.Mul(&t9, &t3) + + t12.Mul(&t11, &t9) + + t13.Mul(&t14, &t12) + + t1.Mul(&t1, &t13) + + t11.Mul(&t0, &t1) + + t14.Mul(&t14, &t11) + + t15.Mul(&t4, &t14) + + t4.Mul(&t0, &t11) + + t0.Mul(&t0, &t15) + + for range 8 { + t15.Square(&t15) + } + + t14.Mul(&t14, &t15) + + for range 9 { + t14.Square(&t14) + } + + t13.Mul(&t13, &t14) + + for range 9 { + t13.Square(&t13) + } + + t13.Mul(&t12, &t13) + + for range 9 { + t13.Square(&t13) + } + + t12.Mul(&t12, &t13) + + for range 8 { + t12.Square(&t12) + } + + t11.Mul(&t11, &t12) + + for range 6 { + t11.Square(&t11) + } + + t10.Mul(&t10, &t11) + + for range 10 { + t10.Square(&t10) + } + + t9.Mul(&t9, &t10) + + for range 9 { + t9.Square(&t9) + } + + t8.Mul(&t8, &t9) + + for range 8 { + t8.Square(&t8) + } + + t8.Mul(&t6, &t8) + + for range 8 { + t8.Square(&t8) + } + + t8.Mul(&x, &t8) + + for range 14 { + t8.Square(&t8) + } + + t7.Mul(&t7, &t8) + + for range 10 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 15 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 10 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 8 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + for range 16 { + t3.Square(&t3) + } + + t3.Mul(&t1, &t3) + + for range 8 { + t3.Square(&t3) + } + + t3.Mul(&t0, &t3) + + for range 7 { + t3.Square(&t3) + } + + t2.Mul(&t2, &t3) + + for range 9 { + t2.Square(&t2) + } + + t2.Mul(&t0, &t2) + + for range 8 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 8 { + t1.Square(&t1) + } + + t1.Mul(&t0, &t1) + + for range 8 { + t1.Square(&t1) + } + + t1.Mul(&t0, &t1) + + for range 8 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + for range 2 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + for range 29 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() +} diff --git a/ecc/bls24-315/twistededwards/curve.go b/ecc/bls24-315/twistededwards/curve.go index 0965104a9f..75842416b4 100644 --- a/ecc/bls24-315/twistededwards/curve.go +++ b/ecc/bls24-315/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bls24-315/twistededwards/point_test.go b/ecc/bls24-315/twistededwards/point_test.go index da9ee08ec8..8a819c5fc0 100644 --- a/ecc/bls24-315/twistededwards/point_test.go +++ b/ecc/bls24-315/twistededwards/point_test.go @@ -823,6 +823,161 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1231,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bls24-315/twistededwards/subgroup.go b/ecc/bls24-315/twistededwards/subgroup.go new file mode 100644 index 0000000000..b057616c79 --- /dev/null +++ b/ecc/bls24-315/twistededwards/subgroup.go @@ -0,0 +1,497 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + initCofactorEightTorsion(&t4) +} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _100 = 1 + _11 + // _101 = 1 + _100 + // _110 = 1 + _101 + // _1001 = _11 + _110 + // _1011 = _10 + _1001 + // _1111 = _100 + _1011 + // _10011 = _100 + _1111 + // _10101 = _10 + _10011 + // _11001 = _100 + _10101 + // _11011 = _10 + _11001 + // _100001 = _110 + _11011 + // _100111 = _110 + _100001 + // _101011 = _100 + _100111 + // _110001 = _110 + _101011 + // _110011 = _10 + _110001 + // _111001 = _110 + _110011 + // _111011 = _10 + _111001 + // _111101 = _10 + _111011 + // _111111 = _10 + _111101 + // _1100100 = _100111 + _111101 + // _1111111 = _11011 + _1100100 + // i41 = ((_1100100 << 4 + _11011) << 7 + _111101) << 5 + // i59 = ((_1011 + i41) << 8 + _1001) << 7 + _10101 + // i84 = ((i59 << 8 + _111011) << 7 + _100001) << 8 + // i101 = ((_101011 + i84) << 6 + _1001) << 8 + _1111111 + // i126 = ((i101 << 9 + _111111) << 6 + _11001) << 8 + // i139 = ((_111001 + i126) << 4 + _1111) << 6 + _1001 + // i163 = ((i139 << 8 + _111101) << 6 + _10011) << 8 + // i178 = ((_11001 + i163) << 9 + _110001) << 3 + 1 + // i205 = ((i178 << 13 + _111011) << 8 + _111001) << 4 + // i225 = ((_1001 + i205) << 9 + _100111) << 8 + _11011 + // i244 = ((i225 << 3 + 1) << 11 + _110011) << 3 + // i265 = ((_101 + i244) << 10 + _110001) << 8 + _1111111 + // return ((i265 << 2 + 1) << 10 + _11) << 19 + // + // Operations: 244 squares 54 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18 fr.Element + + t0.Square(&x) + + z.Mul(&x, &t0) + + t1.Mul(&x, z) + + t2.Mul(&x, &t1) + + t7.Mul(&x, &t2) + + t6.Mul(z, &t7) + + t17.Mul(&t0, &t6) + + t12.Mul(&t1, &t17) + + t10.Mul(&t1, &t12) + + t16.Mul(&t0, &t10) + + t9.Mul(&t1, &t16) + + t4.Mul(&t0, &t9) + + t15.Mul(&t7, &t4) + + t5.Mul(&t7, &t15) + + t14.Mul(&t1, &t5) + + t1.Mul(&t7, &t14) + + t3.Mul(&t0, &t1) + + t7.Mul(&t7, &t3) + + t8.Mul(&t0, &t7) + + t11.Mul(&t0, &t8) + + t13.Mul(&t0, &t11) + + t18.Mul(&t5, &t11) + + t0.Mul(&t4, &t18) + + for range 4 { + t18.Square(&t18) + } + + t18.Mul(&t4, &t18) + + for range 7 { + t18.Square(&t18) + } + + t18.Mul(&t11, &t18) + + for range 5 { + t18.Square(&t18) + } + + t17.Mul(&t17, &t18) + + for range 8 { + t17.Square(&t17) + } + + t17.Mul(&t6, &t17) + + for range 7 { + t17.Square(&t17) + } + + t16.Mul(&t16, &t17) + + for range 8 { + t16.Square(&t16) + } + + t16.Mul(&t8, &t16) + + for range 7 { + t16.Square(&t16) + } + + t15.Mul(&t15, &t16) + + for range 8 { + t15.Square(&t15) + } + + t14.Mul(&t14, &t15) + + for range 6 { + t14.Square(&t14) + } + + t14.Mul(&t6, &t14) + + for range 8 { + t14.Square(&t14) + } + + t14.Mul(&t0, &t14) + + for range 9 { + t14.Square(&t14) + } + + t13.Mul(&t13, &t14) + + for range 6 { + t13.Square(&t13) + } + + t13.Mul(&t9, &t13) + + for range 8 { + t13.Square(&t13) + } + + t13.Mul(&t7, &t13) + + for range 4 { + t13.Square(&t13) + } + + t12.Mul(&t12, &t13) + + for range 6 { + t12.Square(&t12) + } + + t12.Mul(&t6, &t12) + + for range 8 { + t12.Square(&t12) + } + + t11.Mul(&t11, &t12) + + for range 6 { + t11.Square(&t11) + } + + t10.Mul(&t10, &t11) + + for range 8 { + t10.Square(&t10) + } + + t9.Mul(&t9, &t10) + + for range 9 { + t9.Square(&t9) + } + + t9.Mul(&t1, &t9) + + for range 3 { + t9.Square(&t9) + } + + t9.Mul(&x, &t9) + + for range 13 { + t9.Square(&t9) + } + + t8.Mul(&t8, &t9) + + for range 8 { + t8.Square(&t8) + } + + t7.Mul(&t7, &t8) + + for range 4 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 9 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 8 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 3 { + t4.Square(&t4) + } + + t4.Mul(&x, &t4) + + for range 11 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + for range 3 { + t3.Square(&t3) + } + + t2.Mul(&t2, &t3) + + for range 10 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 8 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + for range 2 { + t0.Square(&t0) + } + + t0.Mul(&x, &t0) + + for range 10 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + for range 19 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() +} diff --git a/ecc/bls24-317/twistededwards/curve.go b/ecc/bls24-317/twistededwards/curve.go index f01c3ac848..df7e9326c4 100644 --- a/ecc/bls24-317/twistededwards/curve.go +++ b/ecc/bls24-317/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bls24-317/twistededwards/point_test.go b/ecc/bls24-317/twistededwards/point_test.go index 132516a828..848f4648eb 100644 --- a/ecc/bls24-317/twistededwards/point_test.go +++ b/ecc/bls24-317/twistededwards/point_test.go @@ -823,6 +823,161 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1231,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bls24-317/twistededwards/subgroup.go b/ecc/bls24-317/twistededwards/subgroup.go new file mode 100644 index 0000000000..17e8b44a26 --- /dev/null +++ b/ecc/bls24-317/twistededwards/subgroup.go @@ -0,0 +1,459 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + initCofactorEightTorsion(&t4) +} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _101 = _10 + _11 + // _110 = 1 + _101 + // _1001 = _11 + _110 + // _1010 = 1 + _1001 + // _1011 = 1 + _1010 + // _1111 = _101 + _1010 + // _10001 = _10 + _1111 + // _11011 = _1010 + _10001 + // _11101 = _10 + _11011 + // _100011 = _110 + _11101 + // _101001 = _110 + _100011 + // _101101 = _1010 + _100011 + // _101111 = _10 + _101101 + // _110101 = _110 + _101111 + // _111001 = _1010 + _101111 + // _111111 = _110 + _111001 + // _1111110 = 2*_111111 + // _1111111 = 1 + _1111110 + // _10001000 = _1001 + _1111111 + // i45 = ((_10001000 << 8 + _1111111) << 7 + _10001) << 7 + // i58 = ((_111111 + i45) << 7 + _101001) << 3 + _101 + // i77 = ((i58 << 8 + _11011) << 7 + _101111) << 2 + // i98 = ((_11 + i77) << 10 + _101101) << 8 + _1011 + // i121 = ((i98 << 8 + _1001) << 8 + _1111111) << 5 + // i141 = ((_101 + i121) << 8 + _11011) << 9 + _1111 + // i166 = ((i141 << 8 + _110101) << 6 + _1001) << 9 + // i179 = ((_111001 + i166) << 3 + _101) << 7 + _1111 + // i203 = ((i179 << 6 + _1111) << 8 + _100011) << 8 + // i223 = ((_101101 + i203) << 7 + _111111) << 10 + _111001 + // return ((i223 << 5 + _11101) << 5 + _1111) << 57 + // + // Operations: 246 squares 46 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15 fr.Element + + t7.Square(&x) + + t11.Mul(&x, &t7) + + t5.Mul(&t7, &t11) + + t2.Mul(&x, &t5) + + t6.Mul(&t11, &t2) + + t1.Mul(&x, &t6) + + t10.Mul(&x, &t1) + + z.Mul(&t5, &t1) + + t14.Mul(&t7, z) + + t8.Mul(&t1, &t14) + + t0.Mul(&t7, &t8) + + t4.Mul(&t2, &t0) + + t13.Mul(&t2, &t4) + + t3.Mul(&t1, &t4) + + t12.Mul(&t7, &t3) + + t7.Mul(&t2, &t12) + + t1.Mul(&t1, &t12) + + t2.Mul(&t2, &t1) + + t9.Square(&t2) + + t9.Mul(&x, &t9) + + t15.Mul(&t6, &t9) + + for range 8 { + t15.Square(&t15) + } + + t15.Mul(&t9, &t15) + + for range 7 { + t15.Square(&t15) + } + + t14.Mul(&t14, &t15) + + for range 7 { + t14.Square(&t14) + } + + t14.Mul(&t2, &t14) + + for range 7 { + t14.Square(&t14) + } + + t13.Mul(&t13, &t14) + + for range 3 { + t13.Square(&t13) + } + + t13.Mul(&t5, &t13) + + for range 8 { + t13.Square(&t13) + } + + t13.Mul(&t8, &t13) + + for range 7 { + t13.Square(&t13) + } + + t12.Mul(&t12, &t13) + + for range 2 { + t12.Square(&t12) + } + + t11.Mul(&t11, &t12) + + for range 10 { + t11.Square(&t11) + } + + t11.Mul(&t3, &t11) + + for range 8 { + t11.Square(&t11) + } + + t10.Mul(&t10, &t11) + + for range 8 { + t10.Square(&t10) + } + + t10.Mul(&t6, &t10) + + for range 8 { + t10.Square(&t10) + } + + t9.Mul(&t9, &t10) + + for range 5 { + t9.Square(&t9) + } + + t9.Mul(&t5, &t9) + + for range 8 { + t9.Square(&t9) + } + + t8.Mul(&t8, &t9) + + for range 9 { + t8.Square(&t8) + } + + t8.Mul(z, &t8) + + for range 8 { + t8.Square(&t8) + } + + t7.Mul(&t7, &t8) + + for range 6 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 9 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 3 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 7 { + t5.Square(&t5) + } + + t5.Mul(z, &t5) + + for range 6 { + t5.Square(&t5) + } + + t5.Mul(z, &t5) + + for range 8 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 8 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + for range 7 { + t3.Square(&t3) + } + + t2.Mul(&t2, &t3) + + for range 10 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 5 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + for range 5 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + for range 57 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() +} diff --git a/ecc/bn254/twistededwards/curve.go b/ecc/bn254/twistededwards/curve.go index 1a3ae9637f..d50a12fd15 100644 --- a/ecc/bn254/twistededwards/curve.go +++ b/ecc/bn254/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bn254/twistededwards/point_test.go b/ecc/bn254/twistededwards/point_test.go index 83cef096eb..66a1c841cc 100644 --- a/ecc/bn254/twistededwards/point_test.go +++ b/ecc/bn254/twistededwards/point_test.go @@ -823,6 +823,161 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1231,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bn254/twistededwards/subgroup.go b/ecc/bn254/twistededwards/subgroup.go new file mode 100644 index 0000000000..f7fe449c86 --- /dev/null +++ b/ecc/bn254/twistededwards/subgroup.go @@ -0,0 +1,508 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bn254/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + initCofactorEightTorsion(&t4) +} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _101 = _10 + _11 + // _111 = _10 + _101 + // _1001 = _10 + _111 + // _1011 = _10 + _1001 + // _1101 = _10 + _1011 + // _1111 = _10 + _1101 + // _11000 = _1001 + _1111 + // _11111 = _111 + _11000 + // i26 = ((_11000 << 4 + _11) << 3 + 1) << 7 + // i36 = ((_1001 + i26) << 2 + _11) << 5 + _111 + // i53 = (2*(i36 << 6 + _1011) + 1) << 8 + // i64 = (2*(_1001 + i53) + 1) << 7 + _1101 + // i84 = ((i64 << 10 + _101) << 6 + _1101) << 2 + // i100 = ((_11 + i84) << 7 + _101) << 6 + 1 + // i117 = ((i100 << 7 + _1011) << 5 + _1101) << 3 + // i137 = ((_101 + i117) << 8 + _11) << 9 + _101 + // i153 = ((i137 << 3 + _11) << 8 + _1011) << 3 + // i168 = ((_101 + i153) << 5 + _101) << 7 + _11 + // i187 = ((i168 << 7 + _11111) << 2 + 1) << 8 + // i204 = ((_1001 + i187) << 8 + _1111) << 6 + _1101 + // i215 = 2*((i204 << 2 + _11) << 6 + _1011) + // i232 = ((1 + i215) << 8 + _1001) << 6 + _101 + // i257 = ((i232 << 9 + _11111) << 9 + _11111) << 5 + // i270 = ((_1011 + i257) << 3 + 1) << 7 + _11111 + // return (2*i270 + 1) << 25 + // + // Operations: 247 squares 50 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element + + z.Square(&x) + + t3.Mul(&x, z) + + t1.Mul(z, &t3) + + t6.Mul(z, &t1) + + t2.Mul(z, &t6) + + t0.Mul(z, &t2) + + t4.Mul(z, &t0) + + t5.Mul(z, &t4) + + t7.Mul(&t2, &t5) + + z.Mul(&t6, &t7) + + for range 4 { + t7.Square(&t7) + } + + t7.Mul(&t3, &t7) + + for range 3 { + t7.Square(&t7) + } + + t7.Mul(&x, &t7) + + for range 7 { + t7.Square(&t7) + } + + t7.Mul(&t2, &t7) + + for range 2 { + t7.Square(&t7) + } + + t7.Mul(&t3, &t7) + + for range 5 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 6 { + t6.Square(&t6) + } + + t6.Mul(&t0, &t6) + + t6.Square(&t6) + + t6.Mul(&x, &t6) + + for range 8 { + t6.Square(&t6) + } + + t6.Mul(&t2, &t6) + + t6.Square(&t6) + + t6.Mul(&x, &t6) + + for range 7 { + t6.Square(&t6) + } + + t6.Mul(&t4, &t6) + + for range 10 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 6 { + t6.Square(&t6) + } + + t6.Mul(&t4, &t6) + + for range 2 { + t6.Square(&t6) + } + + t6.Mul(&t3, &t6) + + for range 7 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 6 { + t6.Square(&t6) + } + + t6.Mul(&x, &t6) + + for range 7 { + t6.Square(&t6) + } + + t6.Mul(&t0, &t6) + + for range 5 { + t6.Square(&t6) + } + + t6.Mul(&t4, &t6) + + for range 3 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 8 { + t6.Square(&t6) + } + + t6.Mul(&t3, &t6) + + for range 9 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 3 { + t6.Square(&t6) + } + + t6.Mul(&t3, &t6) + + for range 8 { + t6.Square(&t6) + } + + t6.Mul(&t0, &t6) + + for range 3 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 5 { + t6.Square(&t6) + } + + t6.Mul(&t1, &t6) + + for range 7 { + t6.Square(&t6) + } + + t6.Mul(&t3, &t6) + + for range 7 { + t6.Square(&t6) + } + + t6.Mul(z, &t6) + + for range 2 { + t6.Square(&t6) + } + + t6.Mul(&x, &t6) + + for range 8 { + t6.Square(&t6) + } + + t6.Mul(&t2, &t6) + + for range 8 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 6 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 2 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + for range 6 { + t3.Square(&t3) + } + + t3.Mul(&t0, &t3) + + t3.Square(&t3) + + t3.Mul(&x, &t3) + + for range 8 { + t3.Square(&t3) + } + + t2.Mul(&t2, &t3) + + for range 6 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 9 { + t1.Square(&t1) + } + + t1.Mul(z, &t1) + + for range 9 { + t1.Square(&t1) + } + + t1.Mul(z, &t1) + + for range 5 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + for range 3 { + t0.Square(&t0) + } + + t0.Mul(&x, &t0) + + for range 7 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + z.Square(z) + + z.Mul(&x, z) + + for range 25 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() +} diff --git a/ecc/bw6-633/twistededwards/curve.go b/ecc/bw6-633/twistededwards/curve.go index d491df1d26..aaa2c97cf4 100644 --- a/ecc/bw6-633/twistededwards/curve.go +++ b/ecc/bw6-633/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bw6-633/twistededwards/point_test.go b/ecc/bw6-633/twistededwards/point_test.go index dc58b8a180..5aa530ffe8 100644 --- a/ecc/bw6-633/twistededwards/point_test.go +++ b/ecc/bw6-633/twistededwards/point_test.go @@ -823,6 +823,161 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1231,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bw6-633/twistededwards/subgroup.go b/ecc/bw6-633/twistededwards/subgroup.go new file mode 100644 index 0000000000..825e7fc320 --- /dev/null +++ b/ecc/bw6-633/twistededwards/subgroup.go @@ -0,0 +1,556 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + initCofactorEightTorsion(&t4) +} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _101 = _10 + _11 + // _110 = 1 + _101 + // _1011 = _101 + _110 + // _1101 = _10 + _1011 + // _10001 = _110 + _1011 + // _10011 = _10 + _10001 + // _11001 = _110 + _10011 + // _11011 = _10 + _11001 + // _11101 = _10 + _11011 + // _11111 = _10 + _11101 + // _100001 = _10 + _11111 + // _100011 = _10 + _100001 + // _100101 = _10 + _100011 + // _101001 = _110 + _100011 + // _101011 = _10 + _101001 + // _101111 = _110 + _101001 + // _110101 = _110 + _101111 + // _110111 = _10 + _110101 + // _111011 = _110 + _110101 + // _111101 = _10 + _111011 + // _111111 = _10 + _111101 + // _1111110 = 2*_111111 + // _1111111 = 1 + _1111110 + // _10011000 = _11001 + _1111111 + // i51 = ((_10011000 << 7 + _100011) << 3 + _101) << 13 + // i68 = ((_101011 + i51) << 5 + _1011) << 9 + _11011 + // i88 = ((i68 << 5 + _1011) << 5 + _101) << 8 + // i105 = ((_1101 + i88) << 8 + _111111) << 6 + _11101 + // i129 = ((i105 << 7 + _10011) << 9 + _101111) << 6 + // i147 = ((_101001 + i129) << 6 + _11111) << 9 + _101111 + // i168 = ((i147 << 7 + _101011) << 6 + _111101) << 6 + // i194 = ((_111011 + i168) << 10 + _11101) << 13 + _110101 + // i220 = ((i194 << 6 + _10001) << 8 + _111101) << 10 + // i237 = ((_110101 + i220) << 2 + _11) << 12 + _100001 + // i261 = ((i237 << 10 + _111101) << 5 + _11001) << 7 + // i279 = ((_111011 + i261) << 7 + _100101) << 8 + _101011 + // i304 = ((i279 << 6 + _110111) << 6 + _100101) << 11 + // i317 = ((_10011 + i304) << 8 + _1111111) << 2 + 1 + // i338 = 2*((i317 << 10 + 1) << 8 + _1111111) + // i353 = ((1 + i338) << 2 + 1) << 10 + _11 + // return i353 << 17 + // + // Operations: 306 squares 64 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21 fr.Element + + t0.Square(&x) + + z.Mul(&x, &t0) + + t17.Mul(&t0, z) + + t5.Mul(&x, &t17) + + t18.Mul(&t17, &t5) + + t16.Mul(&t0, &t18) + + t10.Mul(&t5, &t18) + + t1.Mul(&t0, &t10) + + t6.Mul(&t5, &t1) + + t19.Mul(&t0, &t6) + + t11.Mul(&t0, &t19) + + t13.Mul(&t0, &t11) + + t8.Mul(&t0, &t13) + + t20.Mul(&t0, &t8) + + t2.Mul(&t0, &t20) + + t14.Mul(&t5, &t20) + + t4.Mul(&t0, &t14) + + t12.Mul(&t5, &t14) + + t9.Mul(&t5, &t12) + + t3.Mul(&t0, &t9) + + t5.Mul(&t5, &t9) + + t7.Mul(&t0, &t5) + + t15.Mul(&t0, &t7) + + t0.Square(&t15) + + t0.Mul(&x, &t0) + + t21.Mul(&t6, &t0) + + for range 7 { + t21.Square(&t21) + } + + t20.Mul(&t20, &t21) + + for range 3 { + t20.Square(&t20) + } + + t20.Mul(&t17, &t20) + + for range 13 { + t20.Square(&t20) + } + + t20.Mul(&t4, &t20) + + for range 5 { + t20.Square(&t20) + } + + t20.Mul(&t18, &t20) + + for range 9 { + t20.Square(&t20) + } + + t19.Mul(&t19, &t20) + + for range 5 { + t19.Square(&t19) + } + + t18.Mul(&t18, &t19) + + for range 5 { + t18.Square(&t18) + } + + t17.Mul(&t17, &t18) + + for range 8 { + t17.Square(&t17) + } + + t16.Mul(&t16, &t17) + + for range 8 { + t16.Square(&t16) + } + + t15.Mul(&t15, &t16) + + for range 6 { + t15.Square(&t15) + } + + t15.Mul(&t11, &t15) + + for range 7 { + t15.Square(&t15) + } + + t15.Mul(&t1, &t15) + + for range 9 { + t15.Square(&t15) + } + + t15.Mul(&t12, &t15) + + for range 6 { + t15.Square(&t15) + } + + t14.Mul(&t14, &t15) + + for range 6 { + t14.Square(&t14) + } + + t13.Mul(&t13, &t14) + + for range 9 { + t13.Square(&t13) + } + + t12.Mul(&t12, &t13) + + for range 7 { + t12.Square(&t12) + } + + t12.Mul(&t4, &t12) + + for range 6 { + t12.Square(&t12) + } + + t12.Mul(&t7, &t12) + + for range 6 { + t12.Square(&t12) + } + + t12.Mul(&t5, &t12) + + for range 10 { + t12.Square(&t12) + } + + t11.Mul(&t11, &t12) + + for range 13 { + t11.Square(&t11) + } + + t11.Mul(&t9, &t11) + + for range 6 { + t11.Square(&t11) + } + + t10.Mul(&t10, &t11) + + for range 8 { + t10.Square(&t10) + } + + t10.Mul(&t7, &t10) + + for range 10 { + t10.Square(&t10) + } + + t9.Mul(&t9, &t10) + + for range 2 { + t9.Square(&t9) + } + + t9.Mul(z, &t9) + + for range 12 { + t9.Square(&t9) + } + + t8.Mul(&t8, &t9) + + for range 10 { + t8.Square(&t8) + } + + t7.Mul(&t7, &t8) + + for range 5 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 7 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 7 { + t5.Square(&t5) + } + + t5.Mul(&t2, &t5) + + for range 8 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 6 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + for range 6 { + t3.Square(&t3) + } + + t2.Mul(&t2, &t3) + + for range 11 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 8 { + t1.Square(&t1) + } + + t1.Mul(&t0, &t1) + + for range 2 { + t1.Square(&t1) + } + + t1.Mul(&x, &t1) + + for range 10 { + t1.Square(&t1) + } + + t1.Mul(&x, &t1) + + for range 8 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + t0.Square(&t0) + + t0.Mul(&x, &t0) + + for range 2 { + t0.Square(&t0) + } + + t0.Mul(&x, &t0) + + for range 10 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + for range 17 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() +} diff --git a/ecc/bw6-761/twistededwards/curve.go b/ecc/bw6-761/twistededwards/curve.go index e5a370d33e..8d4aa44a8d 100644 --- a/ecc/bw6-761/twistededwards/curve.go +++ b/ecc/bw6-761/twistededwards/curve.go @@ -36,8 +36,9 @@ func GetEdwardsCurve() CurveParams { } var ( - initOnce sync.Once - curveParams CurveParams + initOnce sync.Once + curveParams CurveParams + subgroupInitOnce sync.Once ) func initCurveParams() { diff --git a/ecc/bw6-761/twistededwards/point_test.go b/ecc/bw6-761/twistededwards/point_test.go index 0ace001441..1c47bfc2e1 100644 --- a/ecc/bw6-761/twistededwards/point_test.go +++ b/ecc/bw6-761/twistededwards/point_test.go @@ -823,6 +823,161 @@ func TestOps(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} + +func TestIsInSubGroup(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genS := GenBigInt() + + properties.Property("Identity element (0,1) should be in subgroup", prop.ForAll( + func() bool { + + var p PointAffine + p.setInfinity() + + return p.IsInSubGroup() + }, + )) + + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + + properties.Property("Test IsInSubGroup", prop.ForAll( + func(s big.Int) bool { + + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() + }, + genS, + )) + + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + + return true + }, + genS, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} func TestMarshal(t *testing.T) { t.Parallel() @@ -1076,3 +1231,33 @@ func BenchmarkIsOnCurve(b *testing.B) { } }) } +func BenchmarkIsInSubGroup(b *testing.B) { + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) + + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } + + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) +} diff --git a/ecc/bw6-761/twistededwards/subgroup.go b/ecc/bw6-761/twistededwards/subgroup.go new file mode 100644 index 0000000000..dfb250c4da --- /dev/null +++ b/ecc/bw6-761/twistededwards/subgroup.go @@ -0,0 +1,571 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package twistededwards + +import "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + initCofactorEightTorsion(&t4) +} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _100 = 1 + _11 + // _101 = 1 + _100 + // _111 = _10 + _101 + // _1001 = _10 + _111 + // _1011 = _10 + _1001 + // _1111 = _100 + _1011 + // _10001 = _10 + _1111 + // _10011 = _10 + _10001 + // _10111 = _100 + _10011 + // _11011 = _100 + _10111 + // _11101 = _10 + _11011 + // _11111 = _10 + _11101 + // _110100 = _10111 + _11101 + // _11010000 = _110100 << 2 + // _11010111 = _111 + _11010000 + // i36 = 2*((_11010111 << 8 + _11101) << 7 + _10001) + // i50 = ((1 + i36) << 9 + _10111) << 2 + _11 + // i71 = ((i50 << 6 + _101) << 4 + 1) << 9 + // i84 = ((_11101 + i71) << 5 + _1011) << 5 + _11 + // i105 = (2*(i84 << 8 + _11101) + 1) << 10 + // i125 = ((_10111 + i105) << 12 + _11011) << 5 + _101 + // i147 = ((i125 << 7 + _101) << 6 + _1001) << 7 + // i158 = ((_11101 + i147) << 5 + _10001) << 3 + _101 + // i181 = ((i158 << 8 + _10001) << 6 + _11011) << 7 + // i200 = ((_11111 + i181) << 4 + _11) << 12 + _1111 + // i219 = ((i200 << 4 + _101) << 8 + _10011) << 5 + // i232 = ((_10001 + i219) << 3 + _111) << 7 + _1111 + // i254 = ((i232 << 5 + _1111) << 7 + _11011) << 8 + // i269 = ((_10001 + i254) << 6 + _11111) << 6 + _11101 + // i304 = ((i269 << 9 + _1001) << 5 + _1001) << 19 + // i321 = ((_10111 + i304) << 8 + _1011) << 6 + _10111 + // i337 = ((i321 << 4 + _101) << 4 + 1) << 6 + // i376 = ((_11 + i337) << 29 + 1) << 7 + _101 + // return (2*(i376 << 9 + _10001) + 1) << 43 + // + // Operations: 369 squares 62 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11 fr.Element + + t6.Square(&x) + + t1.Mul(&x, &t6) + + t5.Mul(&x, &t1) + + t0.Mul(&x, &t5) + + t9.Mul(&t6, &t0) + + t4.Mul(&t6, &t9) + + t3.Mul(&t6, &t4) + + t8.Mul(&t5, &t3) + + z.Mul(&t6, &t8) + + t10.Mul(&t6, z) + + t2.Mul(&t5, &t10) + + t7.Mul(&t5, &t2) + + t5.Mul(&t6, &t7) + + t6.Mul(&t6, &t5) + + t11.Mul(&t2, &t5) + + for range 2 { + t11.Square(&t11) + } + + t11.Mul(&t9, &t11) + + for range 8 { + t11.Square(&t11) + } + + t11.Mul(&t5, &t11) + + for range 7 { + t11.Square(&t11) + } + + t11.Mul(z, &t11) + + t11.Square(&t11) + + t11.Mul(&x, &t11) + + for range 9 { + t11.Square(&t11) + } + + t11.Mul(&t2, &t11) + + for range 2 { + t11.Square(&t11) + } + + t11.Mul(&t1, &t11) + + for range 6 { + t11.Square(&t11) + } + + t11.Mul(&t0, &t11) + + for range 4 { + t11.Square(&t11) + } + + t11.Mul(&x, &t11) + + for range 9 { + t11.Square(&t11) + } + + t11.Mul(&t5, &t11) + + for range 5 { + t11.Square(&t11) + } + + t11.Mul(&t3, &t11) + + for range 5 { + t11.Square(&t11) + } + + t11.Mul(&t1, &t11) + + for range 8 { + t11.Square(&t11) + } + + t11.Mul(&t5, &t11) + + t11.Square(&t11) + + t11.Mul(&x, &t11) + + for range 10 { + t11.Square(&t11) + } + + t11.Mul(&t2, &t11) + + for range 12 { + t11.Square(&t11) + } + + t11.Mul(&t7, &t11) + + for range 5 { + t11.Square(&t11) + } + + t11.Mul(&t0, &t11) + + for range 7 { + t11.Square(&t11) + } + + t11.Mul(&t0, &t11) + + for range 6 { + t11.Square(&t11) + } + + t11.Mul(&t4, &t11) + + for range 7 { + t11.Square(&t11) + } + + t11.Mul(&t5, &t11) + + for range 5 { + t11.Square(&t11) + } + + t11.Mul(z, &t11) + + for range 3 { + t11.Square(&t11) + } + + t11.Mul(&t0, &t11) + + for range 8 { + t11.Square(&t11) + } + + t11.Mul(z, &t11) + + for range 6 { + t11.Square(&t11) + } + + t11.Mul(&t7, &t11) + + for range 7 { + t11.Square(&t11) + } + + t11.Mul(&t6, &t11) + + for range 4 { + t11.Square(&t11) + } + + t11.Mul(&t1, &t11) + + for range 12 { + t11.Square(&t11) + } + + t11.Mul(&t8, &t11) + + for range 4 { + t11.Square(&t11) + } + + t11.Mul(&t0, &t11) + + for range 8 { + t11.Square(&t11) + } + + t10.Mul(&t10, &t11) + + for range 5 { + t10.Square(&t10) + } + + t10.Mul(z, &t10) + + for range 3 { + t10.Square(&t10) + } + + t9.Mul(&t9, &t10) + + for range 7 { + t9.Square(&t9) + } + + t9.Mul(&t8, &t9) + + for range 5 { + t9.Square(&t9) + } + + t8.Mul(&t8, &t9) + + for range 7 { + t8.Square(&t8) + } + + t7.Mul(&t7, &t8) + + for range 8 { + t7.Square(&t7) + } + + t7.Mul(z, &t7) + + for range 6 { + t7.Square(&t7) + } + + t6.Mul(&t6, &t7) + + for range 6 { + t6.Square(&t6) + } + + t5.Mul(&t5, &t6) + + for range 9 { + t5.Square(&t5) + } + + t5.Mul(&t4, &t5) + + for range 5 { + t5.Square(&t5) + } + + t4.Mul(&t4, &t5) + + for range 19 { + t4.Square(&t4) + } + + t4.Mul(&t2, &t4) + + for range 8 { + t4.Square(&t4) + } + + t3.Mul(&t3, &t4) + + for range 6 { + t3.Square(&t3) + } + + t2.Mul(&t2, &t3) + + for range 4 { + t2.Square(&t2) + } + + t2.Mul(&t0, &t2) + + for range 4 { + t2.Square(&t2) + } + + t2.Mul(&x, &t2) + + for range 6 { + t2.Square(&t2) + } + + t1.Mul(&t1, &t2) + + for range 29 { + t1.Square(&t1) + } + + t1.Mul(&x, &t1) + + for range 7 { + t1.Square(&t1) + } + + t0.Mul(&t0, &t1) + + for range 9 { + t0.Square(&t0) + } + + z.Mul(z, &t0) + + z.Square(z) + + z.Mul(&x, z) + + for range 43 { + z.Square(z) + } + + return z +} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() +} diff --git a/internal/generator/addchain/32dbd584953b42564bf8fd939f24f531918901d9cc89c6c833a18bfa0180000 b/internal/generator/addchain/32dbd584953b42564bf8fd939f24f531918901d9cc89c6c833a18bfa0180000 new file mode 100644 index 0000000000..d21a756edd Binary files /dev/null and b/internal/generator/addchain/32dbd584953b42564bf8fd939f24f531918901d9cc89c6c833a18bfa0180000 differ diff --git a/internal/generator/addchain/35c748c2f8a21d58c760b80d94292763445b3e601ea271e3de6c45f741290002e16ba88600000010a1180000000000 b/internal/generator/addchain/35c748c2f8a21d58c760b80d94292763445b3e601ea271e3de6c45f741290002e16ba88600000010a1180000000000 new file mode 100644 index 0000000000..b701f7bef9 Binary files /dev/null and b/internal/generator/addchain/35c748c2f8a21d58c760b80d94292763445b3e601ea271e3de6c45f741290002e16ba88600000010a1180000000000 differ diff --git a/internal/generator/addchain/4aad957a68b2955982d1347970dec00566a9dbfb40000004284600000000000 b/internal/generator/addchain/4aad957a68b2955982d1347970dec00566a9dbfb40000004284600000000000 new file mode 100644 index 0000000000..1b8902c921 Binary files /dev/null and b/internal/generator/addchain/4aad957a68b2955982d1347970dec00566a9dbfb40000004284600000000000 differ diff --git a/internal/generator/addchain/60c89ce5c263405370a08b6d0302b0ba5067d090f372e12287c3eb27e000000 b/internal/generator/addchain/60c89ce5c263405370a08b6d0302b0ba5067d090f372e12287c3eb27e000000 new file mode 100644 index 0000000000..b69e9b6b27 Binary files /dev/null and b/internal/generator/addchain/60c89ce5c263405370a08b6d0302b0ba5067d090f372e12287c3eb27e000000 differ diff --git a/internal/generator/addchain/887f22fd4d1b5f85a1612fe51b079a9239a3cf232d7e1cf5e00000000000000 b/internal/generator/addchain/887f22fd4d1b5f85a1612fe51b079a9239a3cf232d7e1cf5e00000000000000 new file mode 100644 index 0000000000..a7f5393649 Binary files /dev/null and b/internal/generator/addchain/887f22fd4d1b5f85a1612fe51b079a9239a3cf232d7e1cf5e00000000000000 differ diff --git a/internal/generator/addchain/98474056b0daca1a7ee9317d2f8bd5fbd83a03544f435c0843dcbb4a57bca04dfd005fe8060000 b/internal/generator/addchain/98474056b0daca1a7ee9317d2f8bd5fbd83a03544f435c0843dcbb4a57bca04dfd005fe8060000 new file mode 100644 index 0000000000..c8262872a7 Binary files /dev/null and b/internal/generator/addchain/98474056b0daca1a7ee9317d2f8bd5fbd83a03544f435c0843dcbb4a57bca04dfd005fe8060000 differ diff --git a/internal/generator/addchain/e7db4ea6533afa906673b0101343b00aa77b4805fffcb7fdfffffffe0000000 b/internal/generator/addchain/e7db4ea6533afa906673b0101343b00aa77b4805fffcb7fdfffffffe0000000 new file mode 100644 index 0000000000..fdfff20206 Binary files /dev/null and b/internal/generator/addchain/e7db4ea6533afa906673b0101343b00aa77b4805fffcb7fdfffffffe0000000 differ diff --git a/internal/generator/config/bls12-381.go b/internal/generator/config/bls12-381.go index 15d5c3f4b8..7745ef11c4 100644 --- a/internal/generator/config/bls12-381.go +++ b/internal/generator/config/bls12-381.go @@ -155,6 +155,9 @@ var bandersnatch = TwistedEdwardsCurve{ Order: "13108968793781547619861935127046491459309155893440570251786403306729687672801", BaseX: "18886178867200960497001835917649091219057080094937609519140440539760939937304", BaseY: "19188667384257783945677642223292697773471335439753913231509108946878080696678", + Split2TorsionT0: "44968234042453258989421494579017642355260750649112422763795205757285533011434", + Split2TorsionT1: "7467641132672931490026245929168323482429801851415215058808452942653048173085", + Split2TorsionB: "25465760566081946422412445027709227188579564747101592991722834452325077642517", HasEndomorphism: true, Endo0: "37446463827641770816307242315180085052603635617490163568005256780843403514036", Endo1: "49199877423542878313146170939139662862850515542392585932876811575731455068989", diff --git a/internal/generator/config/curve.go b/internal/generator/config/curve.go index e9d77c087f..5534337b88 100644 --- a/internal/generator/config/curve.go +++ b/internal/generator/config/curve.go @@ -3,6 +3,7 @@ package config import ( "math/big" + "github.com/consensys/gnark-crypto/internal/generator/addchain" "github.com/consensys/gnark-crypto/internal/generator/field/config" ) @@ -62,6 +63,17 @@ type TwistedEdwardsCurve struct { A, D, Cofactor, Order, BaseX, BaseY string + // subgroup-test generation metadata + Split2Torsion bool + QuarticExponentData *addchain.AddChainData + OcticExponentData *addchain.AddChainData + + // Split-2-torsion subgroup-check constants. These are curve-specific + // constants used by the existing Koshelev-style cofactor-4 check. + Split2TorsionT0 string + Split2TorsionT1 string + Split2TorsionB string + // set if endomorphism HasEndomorphism bool Endo0, Endo1 string @@ -157,6 +169,25 @@ func (p Point) IsNeg3A() bool { var Curves []Curve var TwistedEdwardsCurves []TwistedEdwardsCurve +func (c TwistedEdwardsCurve) HasSplit2TorsionSubgroupCheck() bool { + return c.Cofactor == "4" && c.Split2Torsion && + c.Split2TorsionT0 != "" && + c.Split2TorsionT1 != "" && + c.Split2TorsionB != "" +} + +func (c TwistedEdwardsCurve) HasNonSplitSubgroupCheck() bool { + return !c.Split2Torsion && (c.Cofactor == "4" || c.Cofactor == "8") +} + +func (c TwistedEdwardsCurve) HasOcticSubgroupCheck() bool { + return c.HasNonSplitSubgroupCheck() && c.Cofactor == "8" +} + +func (c TwistedEdwardsCurve) HasSubgroupCheck() bool { + return c.HasSplit2TorsionSubgroupCheck() || c.HasNonSplitSubgroupCheck() +} + func defaultCRange() []int { // default range for C values in the multiExp return []int{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} @@ -233,9 +264,76 @@ func addCurve(c *Curve) { } func addTwistedEdwardCurve(c *TwistedEdwardsCurve) { + var parent *Curve + for i := range Curves { + if Curves[i].Name == c.Name { + parent = &Curves[i] + break + } + } + if parent == nil { + panic("missing parent curve for twisted Edwards curve " + c.Name) + } + + var modulus, a, d, cofactor big.Int + if _, ok := modulus.SetString(parent.FrModulus, 10); !ok { + panic("invalid fr modulus for twisted Edwards curve " + c.Name) + } + if _, ok := a.SetString(c.A, 10); !ok { + panic("invalid A parameter for twisted Edwards curve " + c.Name) + } + if _, ok := d.SetString(c.D, 10); !ok { + panic("invalid D parameter for twisted Edwards curve " + c.Name) + } + if _, ok := cofactor.SetString(c.Cofactor, 10); !ok { + panic("invalid cofactor for twisted Edwards curve " + c.Name) + } + + a.Mod(&a, &modulus) + d.Mod(&d, &modulus) + var ad big.Int + ad.Mul(&a, &d).Mod(&ad, &modulus) + c.Split2Torsion = legendreSymbol(&ad, &modulus) == 1 + + switch c.Cofactor { + case "4", "8": + if !c.HasSubgroupCheck() { + panic("unsupported twisted Edwards subgroup check configuration for " + c.Name + "/" + c.Package) + } + } + + if c.HasNonSplitSubgroupCheck() { + var exp big.Int + exp.Sub(&modulus, big.NewInt(1)) + switch c.Cofactor { + case "4": + exp.Rsh(&exp, 2) + c.QuarticExponentData = addchain.GetAddChain(&exp) + case "8": + exp.Rsh(&exp, 3) + c.OcticExponentData = addchain.GetAddChain(&exp) + } + } + TwistedEdwardsCurves = append(TwistedEdwardsCurves, *c) } +func legendreSymbol(x, p *big.Int) int { + if x.Sign() == 0 { + return 0 + } + var e, z big.Int + e.Sub(p, big.NewInt(1)).Rsh(&e, 1) + z.Exp(x, &e, p) + if z.Sign() == 0 { + return 0 + } + if z.Cmp(big.NewInt(1)) == 0 { + return 1 + } + return -1 +} + // bigIntToLimbs converts a big.Int to a little-endian slice of uint64 limbs. func bigIntToLimbs(n *big.Int, nLimbs int) []uint64 { limbs := make([]uint64, nLimbs) diff --git a/internal/generator/edwards/generate.go b/internal/generator/edwards/generate.go index a70b51e249..0c26552189 100644 --- a/internal/generator/edwards/generate.go +++ b/internal/generator/edwards/generate.go @@ -2,8 +2,10 @@ package edwards import ( "path/filepath" + stdtemplate "text/template" "github.com/consensys/bavard" + "github.com/consensys/gnark-crypto/internal/generator/addchain" "github.com/consensys/gnark-crypto/internal/generator/common" "github.com/consensys/gnark-crypto/internal/generator/config" "github.com/consensys/gnark-crypto/internal/generator/edwards/template" @@ -16,7 +18,16 @@ func Generate(conf config.TwistedEdwardsCurve, baseDir string, gen *common.Gener {File: filepath.Join(baseDir, "doc.go"), Templates: []string{"doc.go.tmpl"}}, {File: filepath.Join(baseDir, "curve.go"), Templates: []string{"curve.go.tmpl"}}, } + if conf.HasNonSplitSubgroupCheck() { + entries = append(entries, bavard.Entry{File: filepath.Join(baseDir, "subgroup.go"), Templates: []string{"subgroup.go.tmpl"}}) + } edwardsGen := common.NewDefaultGenerator(template.FS) - return edwardsGen.Generate(conf, conf.Package, "", "", entries...) + funcs := make(stdtemplate.FuncMap) + for _, f := range addchain.Functions { + funcs[f.Name] = f.Func + } + return edwardsGen.GenerateWithOptions(conf, conf.Package, "", "", []func(*bavard.Bavard) error{ + bavard.Funcs(funcs), + }, entries...) } diff --git a/internal/generator/edwards/template/curve.go.tmpl b/internal/generator/edwards/template/curve.go.tmpl index 31c7c14f82..eafea804a2 100644 --- a/internal/generator/edwards/template/curve.go.tmpl +++ b/internal/generator/edwards/template/curve.go.tmpl @@ -16,11 +16,9 @@ type CurveParams struct { Cofactor fr.Element Order big.Int Base PointAffine - {{- if eq .Cofactor "4"}} - {{- if eq .Name "bls12-381"}} + {{- if .HasSplit2TorsionSubgroupCheck}} t0, t1, b fr.Element {{- end}} - {{- end}} {{- if .HasEndomorphism}} // endomorphism @@ -41,13 +39,11 @@ func GetEdwardsCurve() CurveParams { res.Cofactor.Set(&curveParams.Cofactor) res.Order.Set(&curveParams.Order) res.Base.Set(&curveParams.Base) - {{- if eq .Cofactor "4"}} - {{- if eq .Name "bls12-381"}} + {{- if .HasSplit2TorsionSubgroupCheck}} res.t0.Set(&curveParams.t0) res.t1.Set(&curveParams.t1) res.b.Set(&curveParams.b) {{- end}} - {{- end}} {{- if .HasEndomorphism}} res.endo[0].Set(&curveParams.endo[0]) @@ -63,6 +59,9 @@ func GetEdwardsCurve() CurveParams { var ( initOnce sync.Once curveParams CurveParams + {{- if .HasNonSplitSubgroupCheck}} + subgroupInitOnce sync.Once + {{- end}} ) @@ -74,12 +73,10 @@ func initCurveParams() { curveParams.Base.X.SetString("{{.BaseX}}") curveParams.Base.Y.SetString("{{.BaseY}}") - {{- if eq .Cofactor "4"}} - {{- if eq .Name "bls12-381"}} - curveParams.t0.SetString("44968234042453258989421494579017642355260750649112422763795205757285533011434") - curveParams.t1.SetString("7467641132672931490026245929168323482429801851415215058808452942653048173085") - curveParams.b.SetString("25465760566081946422412445027709227188579564747101592991722834452325077642517") - {{- end}} + {{- if .HasSplit2TorsionSubgroupCheck}} + curveParams.t0.SetString("{{.Split2TorsionT0}}") + curveParams.t1.SetString("{{.Split2TorsionT1}}") + curveParams.b.SetString("{{.Split2TorsionB}}") {{- end}} {{- if .HasEndomorphism}} diff --git a/internal/generator/edwards/template/point.go.tmpl b/internal/generator/edwards/template/point.go.tmpl index 52f203f735..1d41c2e653 100644 --- a/internal/generator/edwards/template/point.go.tmpl +++ b/internal/generator/edwards/template/point.go.tmpl @@ -161,8 +161,7 @@ func (p *PointAffine) IsOnCurve() bool { return lhs.Equal(&rhs) } -{{- if eq .Cofactor "4"}} -{{- if eq .Name "bls12-381"}} +{{- if .HasSplit2TorsionSubgroupCheck}} // IsInSubGroup checks if a point is in the prime subgroup (and on the curve) // based on https://eprint.iacr.org/2022/037.pdf by D. Koshelev. func (p *PointAffine) IsInSubGroup() bool { @@ -221,7 +220,6 @@ func (p *PointAffine) IsInSubGroup() bool { return tate1.Legendre() == 1 && tate2.Legendre() == 1 } {{- end}} -{{- end}} // Neg sets p to -p1 and returns it func (p *PointAffine) Neg(p1 *PointAffine) *PointAffine { diff --git a/internal/generator/edwards/template/subgroup.go.tmpl b/internal/generator/edwards/template/subgroup.go.tmpl new file mode 100644 index 0000000000..96174aed22 --- /dev/null +++ b/internal/generator/edwards/template/subgroup.go.tmpl @@ -0,0 +1,303 @@ +{{- if .HasNonSplitSubgroupCheck}} + +import "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" + +// weierstrassPointAffine stores coordinates on the short-Weierstrass model +// birationally equivalent to this Edwards curve. It is deliberately distinct +// from PointAffine so Edwards formulas cannot be called on Weierstrass points. +type weierstrassPointAffine struct { + X, Y fr.Element +} + +type subgroupParams struct { + // edwardsAMinusD is the Edwards-model coefficient a-d used by the birational map. + edwardsAMinusD fr.Element + // weierstrassXShift is the x-translation A_M/3 from Montgomery to Weierstrass form. + weierstrassXShift fr.Element + // weierstrassA is the a-coefficient of the working short Weierstrass model. + weierstrassA fr.Element + // torsionPoint2 is the rational 2-torsion point used by the subgroup test. + torsionPoint2 weierstrassPointAffine + // torsionPoint4 is the rational 4-torsion generator used by the subgroup test. + torsionPoint4 weierstrassPointAffine + // tangentSlopeAtT4 is the tangent slope at torsionPoint4 on the working Weierstrass model. + tangentSlopeAtT4 fr.Element + {{- if .HasOcticSubgroupCheck}} + // torsionPoint8 is the rational 8-torsion generator used by the subgroup test. + torsionPoint8 weierstrassPointAffine + // tangentSlopeAtT8 is the tangent slope at torsionPoint8 on the working Weierstrass model. + tangentSlopeAtT8 fr.Element + {{- end}} +} + +var subgroupData subgroupParams + +// initCofactorSubgroupParams precomputes torsion points and tangent lines for +// the divide-and-pair subgroup test described in: +// +// - https://eprint.iacr.org/2026/749 +// - https://hackmd.io/@yelhousni/divide-and-pair +// +// The Edwards curve is mapped to a short-Weierstrass model where the relevant +// torsion basis and Miller functions have simple line/vertical-line formulas. +func initCofactorSubgroupParams() { + var three, inv3 fr.Element + three.SetUint64(3) + inv3.Inverse(&three) + + var montA, montB fr.Element + subgroupData.edwardsAMinusD.Sub(&curveParams.A, &curveParams.D) + montA.Add(&curveParams.A, &curveParams.D).Double(&montA) + montB.Square(&subgroupData.edwardsAMinusD) + + subgroupData.weierstrassXShift.Mul(&montA, &inv3) + subgroupData.weierstrassA.Square(&montA).Mul(&subgroupData.weierstrassA, &inv3).Neg(&subgroupData.weierstrassA) + subgroupData.weierstrassA.Add(&subgroupData.weierstrassA, &montB) + + var sqrtA fr.Element + var t4 PointAffine + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + panic("twisted Edwards subgroup SMT expects a square a-parameter") + } + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + mapEdwardsToWeierstrass(&t4, &subgroupData.torsionPoint4) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT4, &subgroupData.torsionPoint4) + weierstrassDouble(&subgroupData.torsionPoint2, &subgroupData.torsionPoint4) + + {{- if .HasOcticSubgroupCheck}} + initCofactorEightTorsion(&t4) + {{- end}} +} + +{{- if .HasOcticSubgroupCheck}} +func initCofactorEightTorsion(t4 *PointAffine) { + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + panic("failed to initialize 8-torsion point") + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root, ySign, xSign *fr.Element) bool { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return false + } + if !ySign.IsOne() { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return false + } + if !xSign.IsOne() { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return false + } + mapEdwardsToWeierstrass(&q, &subgroupData.torsionPoint8) + weierstrassTangentSlope(&subgroupData.tangentSlopeAtT8, &subgroupData.torsionPoint8) + return true + } + + var pos, neg fr.Element + pos.SetOne() + neg.SetOne().Neg(&neg) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + discrSqrt.Neg(&discrSqrt) + if tryT8(&discrSqrt, &pos, &pos) || tryT8(&discrSqrt, &pos, &neg) || tryT8(&discrSqrt, &neg, &pos) || tryT8(&discrSqrt, &neg, &neg) { + return + } + + panic("failed to find rational 8-torsion generator") +} +{{- end}} + +func mapEdwardsToWeierstrass(p *PointAffine, out *weierstrassPointAffine) { + var one, two, inv2, num, den, denInv, u fr.Element + one.SetOne() + two.SetUint64(2) + inv2.Inverse(&two) + + num.Add(&one, &p.Y) + den.Sub(&one, &p.Y). + Mul(&den, &p.X) + denInv.Inverse(&den) + + out.Y.Set(&subgroupData.edwardsAMinusD). + Mul(&out.Y, &num). + Mul(&out.Y, &two). + Mul(&out.Y, &denInv) + u.Mul(&out.Y, &p.X).Mul(&u, &inv2) + out.X.Add(&u, &subgroupData.weierstrassXShift) +} + +func weierstrassDouble(out, p *weierstrassPointAffine) { + var lambda, tmp fr.Element + weierstrassTangentSlope(&lambda, p) + tmp.Square(&lambda).Sub(&tmp, &p.X).Sub(&tmp, &p.X) + out.X.Set(&tmp) + out.Y.Sub(&p.X, &out.X).Mul(&out.Y, &lambda).Sub(&out.Y, &p.Y) +} + +func weierstrassTangentSlope(lambda *fr.Element, p *weierstrassPointAffine) { + var num, den fr.Element + num.Square(&p.X) + fr.MulBy3(&num) + num.Add(&num, &subgroupData.weierstrassA) + den.Double(&p.Y).Inverse(&den) + lambda.Mul(&num, &den) +} + +func evaluateTangentLine(line, xR, yR *fr.Element, t *weierstrassPointAffine, lambda *fr.Element) { + line.Sub(yR, &t.Y) + var tmp fr.Element + tmp.Sub(xR, &t.X).Mul(&tmp, lambda) + line.Sub(line, &tmp) +} + +func mapPointToWeierstrass(p *PointAffine, out *weierstrassPointAffine) bool { + if p.X.IsZero() { + return false + } + mapEdwardsToWeierstrass(p, out) + return true +} + +{{- if .QuarticExponentData}} +func expByQuarticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + {{- range lines_ (format_ .QuarticExponentData.Script) }} + // {{ . }} + {{- end }} + // + // Operations: {{ .QuarticExponentData.Ops.Doubles }} squares {{ .QuarticExponentData.Ops.Adds }} multiplies + {{- if .QuarticExponentData.Program.Temporaries}} + var {{range $i, $e := .QuarticExponentData.Program.Temporaries }}{{ $e }} {{- if last_ $i $.QuarticExponentData.Program.Temporaries}} fr.Element {{- else }}, {{- end}}{{- end -}} + {{- end}} + {{ range $i := .QuarticExponentData.Program.Instructions }} + {{- with add_ $i.Op }} + {{ $i.Output }}.Mul({{ ptr_ .X }}{{ .X }}, {{ ptr_ .Y }}{{ .Y }}) + {{ end -}} + {{- with double_ $i.Op }} + {{ $i.Output }}.Square({{ ptr_ .X }}{{ .X }}) + {{ end -}} + {{- with shift_ $i.Op -}} + {{- $first := 0 -}} + {{- if ne $i.Output.Identifier .X.Identifier }} + {{ $i.Output }}.Square({{ ptr_ .X }}{{ .X }}) + {{- $first = 1 -}} + {{- end }} + {{- if eq $first 0 }} + for range {{ .S }} { + {{- else }} + for s := 1; s < {{ .S }}; s++ { + {{- end }} + {{ $i.Output }}.Square({{ ptr_ $i.Output }}{{ $i.Output }}) + } + {{ end -}} + {{- end }} + return z +} +{{- end}} + +{{- if .OcticExponentData}} +func expByOcticExp(z *fr.Element, x fr.Element) *fr.Element { + // addition chain: + // + {{- range lines_ (format_ .OcticExponentData.Script) }} + // {{ . }} + {{- end }} + // + // Operations: {{ .OcticExponentData.Ops.Doubles }} squares {{ .OcticExponentData.Ops.Adds }} multiplies + {{- if .OcticExponentData.Program.Temporaries}} + var {{range $i, $e := .OcticExponentData.Program.Temporaries }}{{ $e }} {{- if last_ $i $.OcticExponentData.Program.Temporaries}} fr.Element {{- else }}, {{- end}}{{- end -}} + {{- end}} + {{ range $i := .OcticExponentData.Program.Instructions }} + {{- with add_ $i.Op }} + {{ $i.Output }}.Mul({{ ptr_ .X }}{{ .X }}, {{ ptr_ .Y }}{{ .Y }}) + {{ end -}} + {{- with double_ $i.Op }} + {{ $i.Output }}.Square({{ ptr_ .X }}{{ .X }}) + {{ end -}} + {{- with shift_ $i.Op -}} + {{- $first := 0 -}} + {{- if ne $i.Output.Identifier .X.Identifier }} + {{ $i.Output }}.Square({{ ptr_ .X }}{{ .X }}) + {{- $first = 1 -}} + {{- end }} + {{- if eq $first 0 }} + for range {{ .S }} { + {{- else }} + for s := 1; s < {{ .S }}; s++ { + {{- end }} + {{ $i.Output }}.Square({{ ptr_ $i.Output }}{{ $i.Output }}) + } + {{ end -}} + {{- end }} + return z +} +{{- end}} + +// IsInSubGroup checks if a point is in the prime subgroup. +func (p *PointAffine) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + if p.IsZero() { + return true + } + + initOnce.Do(initCurveParams) + subgroupInitOnce.Do(initCofactorSubgroupParams) + + var r weierstrassPointAffine + if !mapPointToWeierstrass(p, &r) { + return false + } + + {{- if eq .Cofactor "4"}} + var l1, v1, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1) + tmp.Square(&v1).Mul(&tmp, &v1) + z.Mul(&z, &tmp) + expByQuarticExp(&check, z) + return check.IsOne() + {{- else if eq .Cofactor "8"}} + var l1, l2, v1, v2, z, check, tmp fr.Element + evaluateTangentLine(&l1, &r.X, &r.Y, &subgroupData.torsionPoint8, &subgroupData.tangentSlopeAtT8) + evaluateTangentLine(&l2, &r.X, &r.Y, &subgroupData.torsionPoint4, &subgroupData.tangentSlopeAtT4) + v1.Sub(&r.X, &subgroupData.torsionPoint4.X) + v2.Sub(&r.X, &subgroupData.torsionPoint2.X) + z.Square(&l1).Square(&z) + tmp.Square(&v1).Square(&tmp) + z.Mul(&z, &tmp) + tmp.Square(&l2) + z.Mul(&z, &tmp) + tmp.Square(&v2) + var v2Pow7 fr.Element + v2Pow7.Square(&tmp).Mul(&v2Pow7, &tmp).Mul(&v2Pow7, &v2) + tmp.Set(&v2Pow7) + z.Mul(&z, &tmp) + expByOcticExp(&check, z) + return check.IsOne() + {{- end}} +} + +{{- end}} diff --git a/internal/generator/edwards/template/tests/point.go.tmpl b/internal/generator/edwards/template/tests/point.go.tmpl index b5af58a89f..bccd6c021b 100644 --- a/internal/generator/edwards/template/tests/point.go.tmpl +++ b/internal/generator/edwards/template/tests/point.go.tmpl @@ -860,8 +860,74 @@ func TestOps(t *testing.T) { } -{{- if eq .Cofactor "4"}} -{{- if eq .Name "bls12-381"}} +{{- if .HasSubgroupCheck}} +func testSubgroupByOrder(p *PointAffine) bool { + params := GetEdwardsCurve() + var check PointAffine + check.ScalarMultiplication(p, ¶ms.Order) + return check.IsZero() +} + +func testRejectTorsionCoset(p, torsion *PointAffine) bool { + var q PointAffine + q.Add(p, torsion) + return !q.IsInSubGroup() && !testSubgroupByOrder(&q) +} + +{{- if .HasOcticSubgroupCheck}} +func testTorsion8Point(t4 *PointAffine) (PointAffine, bool) { + initOnce.Do(initCurveParams) + var one, aInv, discr, discrSqrt, x2, y2, t fr.Element + one.SetOne() + aInv.Inverse(&curveParams.A) + discr.Set(&curveParams.D).Neg(&discr).Mul(&discr, &aInv).Add(&discr, &one) + if discrSqrt.Sqrt(&discr) == nil { + return PointAffine{}, false + } + + var dInv fr.Element + dInv.Inverse(&curveParams.D) + tryT8 := func(root *fr.Element, negateY, negateX bool) (PointAffine, bool) { + var q, dbl PointAffine + t.Add(&one, root). + Mul(&t, &curveParams.A). + Mul(&t, &dInv) + if y2.Sqrt(&t) == nil { + return PointAffine{}, false + } + if negateY { + y2.Neg(&y2) + } + x2.Mul(&t, &aInv) + if q.X.Sqrt(&x2) == nil { + return PointAffine{}, false + } + if negateX { + q.X.Neg(&q.X) + } + q.Y.Set(&y2) + dbl.Double(&q) + if !dbl.Equal(t4) { + return PointAffine{}, false + } + return q, true + } + + roots := [2]fr.Element{discrSqrt} + roots[1].Neg(&discrSqrt) + for i := range roots { + for _, negateY := range []bool{false, true} { + for _, negateX := range []bool{false, true} { + if q, ok := tryT8(&roots[i], negateY, negateX); ok { + return q, true + } + } + } + } + return PointAffine{}, false +} +{{- end}} + func TestIsInSubGroup(t *testing.T) { t.Parallel() parameters := gopter.DefaultTestParameters() @@ -884,6 +950,14 @@ func TestIsInSubGroup(t *testing.T) { }, )) + properties.Property("The 2-torsion point (0,-1) should not be in subgroup", prop.ForAll( + func() bool { + var p PointAffine + p.Y.SetOne().Neg(&p.Y) + return !p.IsInSubGroup() + }, + )) + properties.Property("Test IsInSubGroup", prop.ForAll( func(s big.Int) bool { @@ -897,10 +971,61 @@ func TestIsInSubGroup(t *testing.T) { genS, )) + properties.Property("IsInSubGroup should agree with multiplication by subgroup order on subgroup points", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + return p.IsInSubGroup() == testSubgroupByOrder(&p) + }, + genS, + )) + + properties.Property("Torsion cosets should not be in subgroup", prop.ForAll( + func(s big.Int) bool { + params := GetEdwardsCurve() + + var p PointAffine + p.ScalarMultiplication(¶ms.Base, &s) + + var t2 PointAffine + t2.Y.SetOne().Neg(&t2.Y) + if !testRejectTorsionCoset(&p, &t2) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + + {{- if .HasNonSplitSubgroupCheck}} + initOnce.Do(initCurveParams) + var sqrtA fr.Element + sqrtA.Set(&curveParams.A) + if sqrtA.Sqrt(&sqrtA) == nil { + return false + } + var t4 PointAffine + t4.Y.SetZero() + t4.X.Inverse(&sqrtA) + if !testRejectTorsionCoset(&p, &t4) { //nolint: staticcheck, we code generate and in some paths we don't return early + return false + } + + {{- if .HasOcticSubgroupCheck}} + t8, ok := testTorsion8Point(&t4) + if !ok || !testRejectTorsionCoset(&p, &t8) { + return false + } + {{- end}} + {{- end}} + + return true + }, + genS, + )) + properties.TestingRun(t, gopter.ConsoleReporter(false)) } {{- end}} -{{- end}} func TestMarshal(t *testing.T) { t.Parallel() @@ -1155,20 +1280,35 @@ func BenchmarkIsOnCurve(b *testing.B) { }) } -{{- if eq .Cofactor "4"}} -{{- if eq .Name "bls12-381"}} +{{- if .HasSubgroupCheck}} func BenchmarkIsInSubGroup(b *testing.B) { - params := GetEdwardsCurve() - var s big.Int - s.SetString("52435875175126190479447705081859658376581184513", 10) + params := GetEdwardsCurve() + var s big.Int + s.SetString("52435875175126190479447705081859658376581184513", 10) + + var point PointAffine + point.ScalarMultiplication(¶ms.Base, &s) - var point PointAffine - point.ScalarMultiplication(¶ms.Base, &s) + if !point.IsInSubGroup() { + b.Fatal("point should be in subgroup") + } - b.ResetTimer() - for range b.N { - _ = point.IsInSubGroup() - } + b.Run("is_in_subgroup", func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = point.IsInSubGroup() + } + }) + + b.Run("mul_by_order", func(b *testing.B) { + var check PointAffine + b.ResetTimer() + for range b.N { + check.ScalarMultiplication(&point, ¶ms.Order) + if !check.IsZero() { + b.Fatal("point should have prime order") + } + } + }) } {{- end}} -{{- end}} diff --git a/internal/generator/field/config/field_config.go b/internal/generator/field/config/field_config.go index 07fe87b87d..c3022ec2bd 100644 --- a/internal/generator/field/config/field_config.go +++ b/internal/generator/field/config/field_config.go @@ -17,7 +17,8 @@ import ( ) var ( - errParseModulus = errors.New("can't parse modulus") + errParseModulus = errors.New("can't parse modulus") + errInvalidModulus = errors.New("modulus must be an odd integer greater than 2") ) // Field precomputed values used in template for code generation of field element APIs @@ -139,6 +140,9 @@ func NewFieldConfig(packageName, elementName, modulus string, useAddChain bool) if _, ok := bModulus.SetString(modulus, 0); !ok { return nil, errParseModulus } + if bModulus.Cmp(big.NewInt(3)) < 0 || bModulus.Bit(0) == 0 { + return nil, errInvalidModulus + } // field info F := &Field{ diff --git a/internal/generator/field/config/field_test.go b/internal/generator/field/config/field_test.go index 11740619fc..2ccc3fd0e9 100644 --- a/internal/generator/field/config/field_test.go +++ b/internal/generator/field/config/field_test.go @@ -55,6 +55,14 @@ func TestIntToMont(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func TestNewFieldConfigRejectsUnsupportedModulus(t *testing.T) { + t.Parallel() + + if _, err := NewFieldConfig("dummy", "DummyElement", "0x2", false); err == nil { + t.Fatal("expected even modulus to be rejected") + } +} + func TestBigIntMatchUint64Slice(t *testing.T) { t.Parallel() @@ -204,7 +212,7 @@ func genField(t *testing.T) gopter.Gen { nbWords := minNbWords + mrand.Intn(maxNbWords-minNbWords) //#nosec G404 -- This is a false positive bitLen := max( // #nosec G404 -- This is a false positive - nbWords*64-mrand.Intn(64), 2) + nbWords*64-mrand.Intn(64), 3) modulus, err := rand.Prime(rand.Reader, bitLen) if err != nil {