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..d1e880fce 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("generatd 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 add + x2, y2, z2 string // hex encoded coordinates of second point to add + 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 }