From 7dd885add54c10fb3570c79e6665b9d36f333ef9 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 4 Sep 2024 01:56:52 -0500 Subject: [PATCH] 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 | 12 ++ dcrec/secp256k1/curve_bench_test.go | 2 +- dcrec/secp256k1/curve_test.go | 274 +++++++++++++++++++++++++++- 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 6b01cfef3..aaf40c223 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -149,6 +149,18 @@ 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 { + // Use the group law that a point minus itself is the point at infinity 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 2a19386a8..5f0ed4df2 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 72bbd8ad9..87773470b 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,229 @@ 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) + continue + } + + // 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) + continue + } + } +} + +// 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 +1096,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 +1107,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 10c5f3564..f5e98adff 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 }