From 74faed9828f9317dfe37d4371bc4b89694df8a3f Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 4 Sep 2024 01:56:45 -0500 Subject: [PATCH 1/5] secp256k1: Move curve benchmarks to own file. This splits the benchmarks related to curve operations into their own file to be more consistent with other benchmarks in the package. --- dcrec/secp256k1/bench_test.go | 158 -------------------------- dcrec/secp256k1/curve_bench_test.go | 168 ++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 158 deletions(-) create mode 100644 dcrec/secp256k1/curve_bench_test.go diff --git a/dcrec/secp256k1/bench_test.go b/dcrec/secp256k1/bench_test.go index e48b282952..9dce1d0245 100644 --- a/dcrec/secp256k1/bench_test.go +++ b/dcrec/secp256k1/bench_test.go @@ -9,164 +9,6 @@ import ( "testing" ) -// BenchmarkAddNonConst benchmarks the secp256k1 curve AddNonConst function with -// Z values of 1 so that the associated optimizations are used. -func BenchmarkAddNonConst(b *testing.B) { - p1 := jacobianPointFromHex( - "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", - "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", - "1", - ) - p2 := jacobianPointFromHex( - "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", - "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", - "1", - ) - - b.ReportAllocs() - b.ResetTimer() - var result JacobianPoint - for i := 0; i < b.N; i++ { - AddNonConst(&p1, &p2, &result) - } -} - -// BenchmarkAddNonConstNotZOne benchmarks the secp256k1 curve AddNonConst -// function with Z values other than one so the optimizations associated with -// Z=1 aren't used. -func BenchmarkAddNonConstNotZOne(b *testing.B) { - x1 := new(FieldVal).SetHex("d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718") - y1 := new(FieldVal).SetHex("5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190") - z1 := new(FieldVal).SetHex("2") - x2 := new(FieldVal).SetHex("91abba6a34b7481d922a4bd6a04899d5a686f6cf6da4e66a0cb427fb25c04bd4") - y2 := new(FieldVal).SetHex("03fede65e30b4e7576a2abefc963ddbf9fdccbf791b77c29beadefe49951f7d1") - z2 := new(FieldVal).SetHex("3") - p1 := MakeJacobianPoint(x1, y1, z1) - p2 := MakeJacobianPoint(x2, y2, z2) - - b.ReportAllocs() - b.ResetTimer() - var result JacobianPoint - for i := 0; i < b.N; i++ { - AddNonConst(&p1, &p2, &result) - } -} - -// BenchmarkScalarBaseMultNonConst benchmarks multiplying a scalar by the base -// point of the curve using whichever variant is active. -func BenchmarkScalarBaseMultNonConst(b *testing.B) { - k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") - - b.ReportAllocs() - b.ResetTimer() - var result JacobianPoint - for i := 0; i < b.N; i++ { - ScalarBaseMultNonConst(k, &result) - } -} - -// BenchmarkScalarBaseMultNonConstFast benchmarks multiplying a scalar by the -// base point of the curve using the fast variant. -func BenchmarkScalarBaseMultNonConstFast(b *testing.B) { - k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") - - b.ReportAllocs() - b.ResetTimer() - var result JacobianPoint - for i := 0; i < b.N; i++ { - scalarBaseMultNonConstFast(k, &result) - } -} - -// BenchmarkScalarBaseMultNonConstSlow benchmarks multiplying a scalar by the -// base point of the curve using the resource-constrained slow variant. -func BenchmarkScalarBaseMultNonConstSlow(b *testing.B) { - k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") - - b.ReportAllocs() - b.ResetTimer() - var result JacobianPoint - for i := 0; i < b.N; i++ { - scalarBaseMultNonConstSlow(k, &result) - } -} - -// BenchmarkSplitK benchmarks decomposing scalars into a balanced length-two -// representation. -func BenchmarkSplitK(b *testing.B) { - // Values computed from the group half order and lambda such that they - // exercise the decomposition edge cases and maximize the bit lengths of the - // produced scalars. - h := "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0" - negOne := new(ModNScalar).NegateVal(oneModN) - halfOrder := hexToModNScalar(h) - halfOrderMOne := new(ModNScalar).Add2(halfOrder, negOne) - halfOrderPOne := new(ModNScalar).Add2(halfOrder, oneModN) - lambdaMOne := new(ModNScalar).Add2(endoLambda, negOne) - lambdaPOne := new(ModNScalar).Add2(endoLambda, oneModN) - negLambda := new(ModNScalar).NegateVal(endoLambda) - halfOrderMOneMLambda := new(ModNScalar).Add2(halfOrderMOne, negLambda) - halfOrderMLambda := new(ModNScalar).Add2(halfOrder, negLambda) - halfOrderPOneMLambda := new(ModNScalar).Add2(halfOrderPOne, negLambda) - lambdaPHalfOrder := new(ModNScalar).Add2(endoLambda, halfOrder) - lambdaPOnePHalfOrder := new(ModNScalar).Add2(lambdaPOne, halfOrder) - scalars := []*ModNScalar{ - new(ModNScalar), // zero - oneModN, // one - negOne, // group order - 1 (aka -1 mod N) - halfOrderMOneMLambda, // group half order - 1 - lambda - halfOrderMLambda, // group half order - lambda - halfOrderPOneMLambda, // group half order + 1 - lambda - halfOrderMOne, // group half order - 1 - halfOrder, // group half order - halfOrderPOne, // group half order + 1 - lambdaMOne, // lambda - 1 - endoLambda, // lambda - lambdaPOne, // lambda + 1 - lambdaPHalfOrder, // lambda + group half order - lambdaPOnePHalfOrder, // lambda + 1 + group half order - } - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i += len(scalars) { - for j := 0; j < len(scalars); j++ { - _, _ = splitK(scalars[j]) - } - } -} - -// BenchmarkScalarMultNonConst benchmarks multiplying a scalar by an arbitrary -// point on the curve. -func BenchmarkScalarMultNonConst(b *testing.B) { - k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") - point := jacobianPointFromHex( - "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", - "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", - "1", - ) - - b.ReportAllocs() - b.ResetTimer() - var result JacobianPoint - for i := 0; i < b.N; i++ { - ScalarMultNonConst(k, &point, &result) - } -} - -// BenchmarkNAF benchmarks conversion of a positive integer into its -// non-adjacent form representation. -func BenchmarkNAF(b *testing.B) { - k := fromHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") - kBytes := k.Bytes() - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - naf(kBytes) - } -} - // BenchmarkPubKeyDecompress benchmarks how long it takes to decompress the y // coordinate from a given public key x coordinate. func BenchmarkPubKeyDecompress(b *testing.B) { diff --git a/dcrec/secp256k1/curve_bench_test.go b/dcrec/secp256k1/curve_bench_test.go new file mode 100644 index 0000000000..19a92913fd --- /dev/null +++ b/dcrec/secp256k1/curve_bench_test.go @@ -0,0 +1,168 @@ +// Copyright (c) 2015-2024 The Decred developers +// Copyright 2013-2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import ( + "testing" +) + +// BenchmarkAddNonConst benchmarks the secp256k1 curve AddNonConst function with +// Z values of 1 so that the associated optimizations are used. +func BenchmarkAddNonConst(b *testing.B) { + p1 := jacobianPointFromHex( + "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + "1", + ) + p2 := jacobianPointFromHex( + "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + "1", + ) + + b.ReportAllocs() + b.ResetTimer() + var result JacobianPoint + for i := 0; i < b.N; i++ { + AddNonConst(&p1, &p2, &result) + } +} + +// BenchmarkAddNonConstNotZOne benchmarks the secp256k1 curve AddNonConst +// function with Z values other than one so the optimizations associated with +// Z=1 aren't used. +func BenchmarkAddNonConstNotZOne(b *testing.B) { + x1 := new(FieldVal).SetHex("d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718") + y1 := new(FieldVal).SetHex("5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190") + z1 := new(FieldVal).SetHex("2") + x2 := new(FieldVal).SetHex("91abba6a34b7481d922a4bd6a04899d5a686f6cf6da4e66a0cb427fb25c04bd4") + y2 := new(FieldVal).SetHex("03fede65e30b4e7576a2abefc963ddbf9fdccbf791b77c29beadefe49951f7d1") + z2 := new(FieldVal).SetHex("3") + p1 := MakeJacobianPoint(x1, y1, z1) + p2 := MakeJacobianPoint(x2, y2, z2) + + b.ReportAllocs() + b.ResetTimer() + var result JacobianPoint + for i := 0; i < b.N; i++ { + AddNonConst(&p1, &p2, &result) + } +} + +// BenchmarkScalarBaseMultNonConst benchmarks multiplying a scalar by the base +// point of the curve using whichever variant is active. +func BenchmarkScalarBaseMultNonConst(b *testing.B) { + k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") + + b.ReportAllocs() + b.ResetTimer() + var result JacobianPoint + for i := 0; i < b.N; i++ { + ScalarBaseMultNonConst(k, &result) + } +} + +// BenchmarkScalarBaseMultNonConstFast benchmarks multiplying a scalar by the +// base point of the curve using the fast variant. +func BenchmarkScalarBaseMultNonConstFast(b *testing.B) { + k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") + + b.ReportAllocs() + b.ResetTimer() + var result JacobianPoint + for i := 0; i < b.N; i++ { + scalarBaseMultNonConstFast(k, &result) + } +} + +// BenchmarkScalarBaseMultNonConstSlow benchmarks multiplying a scalar by the +// base point of the curve using the resource-constrained slow variant. +func BenchmarkScalarBaseMultNonConstSlow(b *testing.B) { + k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") + + b.ReportAllocs() + b.ResetTimer() + var result JacobianPoint + for i := 0; i < b.N; i++ { + scalarBaseMultNonConstSlow(k, &result) + } +} + +// BenchmarkSplitK benchmarks decomposing scalars into a balanced length-two +// representation. +func BenchmarkSplitK(b *testing.B) { + // Values computed from the group half order and lambda such that they + // exercise the decomposition edge cases and maximize the bit lengths of the + // produced scalars. + h := "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0" + negOne := new(ModNScalar).NegateVal(oneModN) + halfOrder := hexToModNScalar(h) + halfOrderMOne := new(ModNScalar).Add2(halfOrder, negOne) + halfOrderPOne := new(ModNScalar).Add2(halfOrder, oneModN) + lambdaMOne := new(ModNScalar).Add2(endoLambda, negOne) + lambdaPOne := new(ModNScalar).Add2(endoLambda, oneModN) + negLambda := new(ModNScalar).NegateVal(endoLambda) + halfOrderMOneMLambda := new(ModNScalar).Add2(halfOrderMOne, negLambda) + halfOrderMLambda := new(ModNScalar).Add2(halfOrder, negLambda) + halfOrderPOneMLambda := new(ModNScalar).Add2(halfOrderPOne, negLambda) + lambdaPHalfOrder := new(ModNScalar).Add2(endoLambda, halfOrder) + lambdaPOnePHalfOrder := new(ModNScalar).Add2(lambdaPOne, halfOrder) + scalars := []*ModNScalar{ + new(ModNScalar), // zero + oneModN, // one + negOne, // group order - 1 (aka -1 mod N) + halfOrderMOneMLambda, // group half order - 1 - lambda + halfOrderMLambda, // group half order - lambda + halfOrderPOneMLambda, // group half order + 1 - lambda + halfOrderMOne, // group half order - 1 + halfOrder, // group half order + halfOrderPOne, // group half order + 1 + lambdaMOne, // lambda - 1 + endoLambda, // lambda + lambdaPOne, // lambda + 1 + lambdaPHalfOrder, // lambda + group half order + lambdaPOnePHalfOrder, // lambda + 1 + group half order + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i += len(scalars) { + for j := 0; j < len(scalars); j++ { + _, _ = splitK(scalars[j]) + } + } +} + +// BenchmarkScalarMultNonConst benchmarks multiplying a scalar by an arbitrary +// point on the curve. +func BenchmarkScalarMultNonConst(b *testing.B) { + k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") + point := jacobianPointFromHex( + "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + "1", + ) + + b.ReportAllocs() + b.ResetTimer() + var result JacobianPoint + for i := 0; i < b.N; i++ { + ScalarMultNonConst(k, &point, &result) + } +} + +// BenchmarkNAF benchmarks conversion of a positive integer into its +// non-adjacent form representation. +func BenchmarkNAF(b *testing.B) { + k := fromHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") + kBytes := k.Bytes() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + naf(kBytes) + } +} From 929026d6ffc90f332a16a22446fa176b520b9d64 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 4 Sep 2024 01:56:46 -0500 Subject: [PATCH 2/5] secp256k1: Move pubkey benchmarks to own file. This move the benchmarks related to public keys into their own file to be more consistent with other benchmarks in the package. It also removes the now empty bench_test.go file. --- dcrec/secp256k1/{bench_test.go => pubkey_bench_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dcrec/secp256k1/{bench_test.go => pubkey_bench_test.go} (100%) diff --git a/dcrec/secp256k1/bench_test.go b/dcrec/secp256k1/pubkey_bench_test.go similarity index 100% rename from dcrec/secp256k1/bench_test.go rename to dcrec/secp256k1/pubkey_bench_test.go From 7e4819e197707001470b7cc98f29dcb416009341 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 4 Sep 2024 01:56:48 -0500 Subject: [PATCH 3/5] secp256k1: Separate affine equality func in tests. This splits the closure that determines if two Jacobian represent the same affine point by first converting them to affine points to a separate func for use in upcoming tests and benchmarks. --- dcrec/secp256k1/curve_test.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/dcrec/secp256k1/curve_test.go b/dcrec/secp256k1/curve_test.go index 54d1ffce7f..72bbd8ad97 100644 --- a/dcrec/secp256k1/curve_test.go +++ b/dcrec/secp256k1/curve_test.go @@ -56,6 +56,18 @@ func jacobianPointFromHex(x, y, z string) JacobianPoint { return p } +// isSameAffinePoint returns whether or not the two Jacobian points represent +// the same affine point without modifying the provided points. It converts the +// points to affine to perform the equality check. +func isSameAffinePoint(p1, p2 *JacobianPoint) bool { + var p1Affine, p2Affine JacobianPoint + p1Affine.Set(p1) + p1Affine.ToAffine() + p2Affine.Set(p2) + p2Affine.ToAffine() + return p1Affine.IsStrictlyEqual(&p2Affine) +} + // IsStrictlyEqual returns whether or not the two Jacobian points are strictly // equal for use in the tests. Recall that several Jacobian points can be equal // in affine coordinates, while not having the same coordinates in projective @@ -771,17 +783,6 @@ func TestScalarMultJacobianRandom(t *testing.T) { } }(t, seed) - // isSamePoint returns whether or not the two Jacobian points represent the - // same affine point without modifying the provided points. - isSamePoint := func(p1, p2 *JacobianPoint) bool { - var p1Affine, p2Affine JacobianPoint - p1Affine.Set(p1) - p1Affine.ToAffine() - p2Affine.Set(p2) - p2Affine.ToAffine() - return p1Affine.IsStrictlyEqual(&p2Affine) - } - // The overall idea is to compute the same point different ways. The // strategy uses two properties: // @@ -827,7 +828,7 @@ func TestScalarMultJacobianRandom(t *testing.T) { // Ensure kP + ((-k)P) = ∞. AddNonConst(&chained, &negChained, &result) - if !isSamePoint(&result, &infinity) { + if !isSameAffinePoint(&result, &infinity) { t.Fatalf("%d: expected point at infinity\ngot (%v, %v, %v)\n", i, result.X, result.Y, result.Z) } @@ -838,14 +839,14 @@ func TestScalarMultJacobianRandom(t *testing.T) { // Ensure the point calculated above matches the product of the scalars // times the base point. scalarBaseMultNonConstFast(product, &result) - if !isSamePoint(&chained, &result) { + if !isSameAffinePoint(&chained, &result) { t.Fatalf("unexpected result \ngot (%v, %v, %v)\n"+ "want (%v, %v, %v)", chained.X, chained.Y, chained.Z, result.X, result.Y, result.Z) } scalarBaseMultNonConstSlow(product, &result) - if !isSamePoint(&chained, &result) { + if !isSameAffinePoint(&chained, &result) { t.Fatalf("unexpected result \ngot (%v, %v, %v)\n"+ "want (%v, %v, %v)", chained.X, chained.Y, chained.Z, result.X, result.Y, result.Z) From ecc257fba695614bbc664d50470d66542cfca4b9 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 4 Sep 2024 01:56:49 -0500 Subject: [PATCH 4/5] secp256k1: Add Jacobian point equivalency bench. --- dcrec/secp256k1/curve_bench_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dcrec/secp256k1/curve_bench_test.go b/dcrec/secp256k1/curve_bench_test.go index 19a92913fd..2a19386a88 100644 --- a/dcrec/secp256k1/curve_bench_test.go +++ b/dcrec/secp256k1/curve_bench_test.go @@ -166,3 +166,26 @@ func BenchmarkNAF(b *testing.B) { naf(kBytes) } } + +// BenchmarkJacobianPointEquivalency benchmarks determining if two Jacobian +// points represent the same affine point. +func BenchmarkJacobianPointEquivalency(b *testing.B) { + // Create two Jacobian points with different Z values that represent the + // same affine point. + point1 := jacobianPointFromHex( + "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + "2", + ) + point2 := jacobianPointFromHex( + "dcc3768780c74a0325e2851edad0dc8a566fa61a9e7fc4a34d13dcb509f99bc7", + "3503be6fb22abd76cb082f8aed63745b9149dd2b037728d32ebfebac99b51f17", + "3", + ) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + isSameAffinePoint(&point1, &point2) + } +} From 809d21b7d5a951baca4dd5f980ff6cb1482e2e75 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 4 Sep 2024 01:56:52 -0500 Subject: [PATCH 5/5] secp256k1: Expose Jacobian point equivalency func. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This exposes a new function on the JacobianPoint type named EquivalentNonConst which efficiently determines if two Jacobian points represent the same affine point without actually converting the points to affine. This provides a significant speedup versus first converting to affine for use cases that need the functionality. One example where it is useful is adaptor signatures. It includes comprehensive tests for edge conditions as well as ongoing randomized testing. The following benchmark shows a before and after comparison of checking Jacobian point equivalency with the new method versus the affine conversion approach: name old time/op new time/op delta -------------------------------------------------------------------------------- JacobianPointEquivalency 17.2µs ± 2% 0.5µs ± 1% -97.24% (p=0.000 n=10+10) --- dcrec/secp256k1/curve.go | 14 ++ dcrec/secp256k1/curve_bench_test.go | 2 +- dcrec/secp256k1/curve_test.go | 272 +++++++++++++++++++++++++++- dcrec/secp256k1/field_test.go | 1 + 4 files changed, 285 insertions(+), 4 deletions(-) diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index 6b01cfef34..b0df22c07e 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -149,6 +149,20 @@ func (p *JacobianPoint) ToAffine() { p.Y.Normalize() } +// EquivalentNonConst returns whether or not two Jacobian points represent the +// same affine point in *non-constant* time. +func (p *JacobianPoint) EquivalentNonConst(other *JacobianPoint) bool { + // Since the point at infinity is the identity element for the group, note + // that P = P + ∞ trivially implies that P - P = ∞. + // + // Use that fact to determine if the points represent the same affine point. + var result JacobianPoint + result.Set(p) + result.Y.Normalize().Negate(1).Normalize() + AddNonConst(&result, other, &result) + return (result.X.IsZero() && result.Y.IsZero()) || result.Z.IsZero() +} + // addZ1AndZ2EqualsOne adds two Jacobian points that are already known to have // z values of 1 and stores the result in the provided result param. That is to // say result = p1 + p2. It performs faster addition than the generic add diff --git a/dcrec/secp256k1/curve_bench_test.go b/dcrec/secp256k1/curve_bench_test.go index 2a19386a88..5f0ed4df23 100644 --- a/dcrec/secp256k1/curve_bench_test.go +++ b/dcrec/secp256k1/curve_bench_test.go @@ -186,6 +186,6 @@ func BenchmarkJacobianPointEquivalency(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - isSameAffinePoint(&point1, &point2) + point1.EquivalentNonConst(&point2) } } diff --git a/dcrec/secp256k1/curve_test.go b/dcrec/secp256k1/curve_test.go index 72bbd8ad97..9f5e11dd79 100644 --- a/dcrec/secp256k1/curve_test.go +++ b/dcrec/secp256k1/curve_test.go @@ -9,6 +9,7 @@ import ( "fmt" "math/big" "math/bits" + "math/rand" mrand "math/rand" "testing" "time" @@ -45,6 +46,50 @@ func isValidJacobianPoint(point *JacobianPoint) bool { return y2.Equals(&result) } +// Rescale rescales the Jacobian point by the provided value for use in the +// tests. The resulting point will be normalized. +func (p *JacobianPoint) Rescale(s *FieldVal) { + // The X coordinate in Jacobian projective coordinates is X/Z^2 while the + // Y coordinate is Y/Z^3. Thus, rescaling a Jacobian point is: + // p.X *= s^2 + // p.Y *= s^3 + // p.Z *= s + sSquared := new(FieldVal).SquareVal(s) + sCubed := new(FieldVal).Mul2(sSquared, s) + p.X.Mul(sSquared).Normalize() + p.Y.Mul(sCubed).Normalize() + p.Z.Mul(s).Normalize() +} + +// randJacobian returns a Jacobian point created from a point generated by the +// passed rng. +func randJacobian(t *testing.T, rng *rand.Rand) *JacobianPoint { + t.Helper() + + // Generate a random point. + privKey, err := generatePrivateKey(rng) + if err != nil { + t.Fatalf("unexpected error generating random Jacobian point: %v", err) + } + pubKey := privKey.PubKey() + + // Generate a random non-zero value and rescale the point with it so it has + // a random Z value. + randZ := randFieldVal(t, rng) + for randZ.IsZero() { + randZ = randFieldVal(t, rng) + } + var pt JacobianPoint + pubKey.AsJacobian(&pt) + pt.Rescale(randZ) + + // Sanity check the result. + if !isValidJacobianPoint(&pt) { + t.Fatal("generated random Jacobian point is not on the curve") + } + return &pt +} + // jacobianPointFromHex decodes the passed big-endian hex strings into a // Jacobian point with its internal fields set to the resulting values. Only // the first 32-bytes are used. @@ -68,6 +113,227 @@ func isSameAffinePoint(p1, p2 *JacobianPoint) bool { return p1Affine.IsStrictlyEqual(&p2Affine) } +// TestEquivalentJacobian ensures determining if two Jacobian points represent +// the same affine point via [JacobianPoint.EquivalentNonConst] works as +// intended for some edge cases and known values. It also verifies in affine +// coordinates as well. +func TestEquivalentJacobian(t *testing.T) { + tests := []struct { + name string // test description + x1, y1, z1 string // hex encoded coordinates of first point to compare + x2, y2, z2 string // hex encoded coordinates of second point to compare + want bool // expected equivalency result + }{{ + name: "∞ != P", + x1: "0", + y1: "0", + z1: "0", + x2: "d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575", + y2: "131c670d414c4546b88ac3ff664611b1c38ceb1c21d76369d7a7a0969d61d97d", + z2: "1", + want: false, + }, { + name: "P != ∞", + x1: "d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575", + y1: "131c670d414c4546b88ac3ff664611b1c38ceb1c21d76369d7a7a0969d61d97d", + z1: "1", + x2: "0", + y2: "0", + z2: "0", + want: false, + }, { + name: "∞ == ∞", + x1: "0", + y1: "0", + z1: "0", + x2: "0", + y2: "0", + z2: "0", + want: true, + }, { + // Same point with z1=z2=1. + name: "P(x, y, 1) == P(x, y, 1)", + x1: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y1: "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + z1: "1", + x2: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y2: "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + z2: "1", + want: true, + }, { + // Same point with z1=z2=2. + name: "P(x, y, 2) == P(x, y, 2)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y2: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z2: "2", + want: true, + }, { + // Same point with different Z values (P1.Z=2, P2.Z=1) + name: "P(x, y, 2) == P(x, y, 1)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y2: "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + z2: "1", + want: true, + }, { + // Same point with different Z values (P1.Z=2, P2.Z=3) + name: "P(x, y, 2) == P(x, y, 3)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "dcc3768780c74a0325e2851edad0dc8a566fa61a9e7fc4a34d13dcb509f99bc7", + y2: "3503be6fb22abd76cb082f8aed63745b9149dd2b037728d32ebfebac99b51f17", + z2: "3", + want: true, + }, { + // Points with different x values and z1=z2=1. + name: "P(x1, y1, 1) != P(x2, y1, 1)", + x1: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y1: "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + z1: "1", + x2: "d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575", + y2: "131c670d414c4546b88ac3ff664611b1c38ceb1c21d76369d7a7a0969d61d97d", + z2: "1", + want: false, + }, { + // Points with different x values and z1=z2=2. + name: "P(x1, y1, 2) != P(x2, y2, 2)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "5d2fe112c21891d440f65a98473cb626111f8a234d2cd82f22172e369f002147", + y2: "98e3386a0a622a35c4561ffb32308d8e1c6758e10ebb1b4ebd3d04b4eb0ecbe8", + z2: "2", + want: false, + }, { + // Points that are opposites with z1=z2=1. + name: "P(x, y, 1) != P(x, -y, 1)", + x1: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y1: "0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232", + z1: "1", + x2: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y2: "f48e156428cf0276dc092da5856e182288d7569f97934a56fe44be60f0d359fd", + z2: "1", + want: false, + }, { + // Points that are opposites with z1=z2=2. + name: "P(x, y, 2) != P(x, -y, 2)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y2: "a470ab21467813b6e0496d2c2b70c11446bab4fcbc9a52b7f225f30e869aea9f", + z2: "2", + want: false, + }, { + // Points with same x, opposite y, and different z values with z2=1. + name: "P(x, y, 2) != P(x, -y, 1)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6", + y2: "f48e156428cf0276dc092da5856e182288d7569f97934a56fe44be60f0d359fd", + z2: "1", + want: false, + }, { + // Points with same x, opposite y, and different z values with z!=1. + name: "P(x, y, 2) + P(x, -y, 3) = ∞", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "dcc3768780c74a0325e2851edad0dc8a566fa61a9e7fc4a34d13dcb509f99bc7", + y2: "cafc41904dd5428934f7d075129c8ba46eb622d4fc88d72cd1401452664add18", + z2: "3", + want: false, + }, { + // Points with all different values. + name: "P(x1, y1, 2) + P(x2, y2, 1)", + x1: "d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718", + y1: "5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190", + z1: "2", + x2: "d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575", + y2: "131c670d414c4546b88ac3ff664611b1c38ceb1c21d76369d7a7a0969d61d97d", + z2: "1", + want: false, + }} + + for _, test := range tests { + // Convert hex to Jacobian points. + p1 := jacobianPointFromHex(test.x1, test.y1, test.z1) + p2 := jacobianPointFromHex(test.x2, test.y2, test.z2) + + // Ensure the test data is using points that are actually on the curve + // (or the point at infinity). + if !isValidJacobianPoint(&p1) { + t.Errorf("%s: first point is not on the curve", test.name) + continue + } + if !isValidJacobianPoint(&p2) { + t.Errorf("%s: second point is not on the curve", test.name) + continue + } + + // Convert the points to affine and ensure they have the expected + // equivalency as well. + got := isSameAffinePoint(&p1, &p2) + if got != test.want { + t.Errorf("%s: mismatched expected test equivalency -- got %v, "+ + "want %v", test.name, got, test.want) + } + + // Ensure the points compare with the expected equivalency without + // converting them to affine. + got2 := p1.EquivalentNonConst(&p2) + if got2 != test.want { + t.Errorf("%s: wrong result -- got %v, want %v", test.name, got2, + test.want) + } + } +} + +// TestEquivalentJacobianRandom ensures determining if two Jacobian points +// represent the same affine point via [JacobianPoint.EquivalentNonConst] works +// as intended for randomly-generated points and rescaled versions of them. +func TestEquivalentJacobianRandom(t *testing.T) { + // Use a unique random seed each test instance and log it if the tests fail. + seed := time.Now().Unix() + rng := mrand.New(mrand.NewSource(seed)) + defer func(t *testing.T, seed int64) { + if t.Failed() { + t.Logf("random seed: %d", seed) + } + }(t, seed) + + for i := 0; i < 100; i++ { + // Generate a pair of random points and ensure the reported Jacobian + // equivalency matches the result of first converting the points to + // affine and checking equality. + pt1, pt2 := randJacobian(t, rng), randJacobian(t, rng) + gotAffine := isSameAffinePoint(pt1, pt2) + gotJacobian := pt1.EquivalentNonConst(pt2) + if gotAffine != gotJacobian { + t.Fatalf("mismatched equivalency -- affine: %v, Jacobian: %v", + gotAffine, gotJacobian) + } + + // Rescale the first point by a random value and ensure it is equivalent + // to the non-rescaled point. + var rescaled JacobianPoint + rescaled.Set(pt1) + rescaled.Rescale(randFieldVal(t, rng)) + rescaledEqual := rescaled.EquivalentNonConst(pt1) + if !rescaledEqual { + t.Fatalf("mismatched equivalency for scaled point -- got %v, want "+ + "true", rescaledEqual) + } + } +} + // IsStrictlyEqual returns whether or not the two Jacobian points are strictly // equal for use in the tests. Recall that several Jacobian points can be equal // in affine coordinates, while not having the same coordinates in projective @@ -828,7 +1094,7 @@ func TestScalarMultJacobianRandom(t *testing.T) { // Ensure kP + ((-k)P) = ∞. AddNonConst(&chained, &negChained, &result) - if !isSameAffinePoint(&result, &infinity) { + if !result.EquivalentNonConst(&infinity) { t.Fatalf("%d: expected point at infinity\ngot (%v, %v, %v)\n", i, result.X, result.Y, result.Z) } @@ -839,14 +1105,14 @@ func TestScalarMultJacobianRandom(t *testing.T) { // Ensure the point calculated above matches the product of the scalars // times the base point. scalarBaseMultNonConstFast(product, &result) - if !isSameAffinePoint(&chained, &result) { + if !chained.EquivalentNonConst(&result) { t.Fatalf("unexpected result \ngot (%v, %v, %v)\n"+ "want (%v, %v, %v)", chained.X, chained.Y, chained.Z, result.X, result.Y, result.Z) } scalarBaseMultNonConstSlow(product, &result) - if !isSameAffinePoint(&chained, &result) { + if !chained.EquivalentNonConst(&result) { t.Fatalf("unexpected result \ngot (%v, %v, %v)\n"+ "want (%v, %v, %v)", chained.X, chained.Y, chained.Z, result.X, result.Y, result.Z) diff --git a/dcrec/secp256k1/field_test.go b/dcrec/secp256k1/field_test.go index 10c5f35647..f5e98adffa 100644 --- a/dcrec/secp256k1/field_test.go +++ b/dcrec/secp256k1/field_test.go @@ -46,6 +46,7 @@ func randFieldVal(t *testing.T, rng *rand.Rand) *FieldVal { // Create and return a field value. var fv FieldVal fv.SetBytes(&buf) + fv.Normalize() return &fv }