Skip to content

Commit

Permalink
secp256k1: Expose EqualsNonConst on jacobian point.
Browse files Browse the repository at this point in the history
This exposes a new function on the JacobianPoint type named
EqualsNonConst 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 equality with the new method versus the affine conversion
approach:

name                    old time/op   new time/op  delta
-----------------------------------------------------------------------------
JacobianPointEquality   17.2µs ± 2%   0.5µs ± 1%   -97.24%  (p=0.000 n=10+10)
  • Loading branch information
davecgh committed Sep 4, 2024
1 parent 7b00c66 commit 22760ba
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 4 deletions.
12 changes: 12 additions & 0 deletions dcrec/secp256k1/curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ func (p *JacobianPoint) ToAffine() {
p.Y.Normalize()
}

// EqualsNonConst returns whether or not two Jacobian points represent the same
// affine point in *non-constant* time.
func (p *JacobianPoint) EqualsNonConst(other *JacobianPoint) bool {
// Use the group law that a point minus itself is the point at infinity to
// determine if the points are equal.
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
Expand Down
2 changes: 1 addition & 1 deletion dcrec/secp256k1/curve_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,6 @@ func BenchmarkJacobianPointEquality(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
isSameAffinePoint(&point1, &point2)
point1.EqualsNonConst(&point2)
}
}
274 changes: 271 additions & 3 deletions dcrec/secp256k1/curve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"math/big"
"math/bits"
"math/rand"
mrand "math/rand"
"testing"
"time"
Expand Down Expand Up @@ -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.
Expand All @@ -68,6 +113,229 @@ func isSameAffinePoint(p1, p2 *JacobianPoint) bool {
return p1Affine.IsStrictlyEqual(&p2Affine)
}

// TestEqualsJacobian ensures determining if two Jacobian points represent the
// same affine point via [JacobianPoint.EqualsNonConst] works as intended for
// some edge cases and known values. It also verifies in affine coordinates as
// well.
func TestEqualsJacobian(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 equality 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
// equality as well.
got := isSameAffinePoint(&p1, &p2)
if got != test.want {
t.Errorf("%s: mismatched expected test equality -- got %v, want %v",
test.name, got, test.want)
continue
}

// Ensure the points compare with the expected equality without
// converting them to affine.
got2 := p1.EqualsNonConst(&p2)
if got2 != test.want {
t.Errorf("%s: wrong result -- got %v, want %v", test.name, got2,
test.want)
continue
}
}
}

// TestEqualsJacobianRandom ensures determining if two Jacobian points represent
// the same affine point via [JacobianPoint.EqualsNonConst] works as intended
// for randomly-generated points and rescaled versions of them.
func TestEqualsJacobianRandom(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
// equality 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.EqualsNonConst(pt2)
if gotAffine != gotJacobian {
t.Fatalf("mismatched equality -- affine: %v, Jacobian: %v",
gotAffine, gotJacobian)
}

// Rescale the first point by a random value and ensure it is equal to
// the non-rescaled point.
var rescaled JacobianPoint
rescaled.Set(pt1)
rescaled.Rescale(randFieldVal(t, rng))
rescaledEqual := rescaled.EqualsNonConst(pt1)
if !rescaledEqual {
t.Fatalf("mismatched equality 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
Expand Down Expand Up @@ -828,7 +1096,7 @@ func TestScalarMultJacobianRandom(t *testing.T) {

// Ensure kP + ((-k)P) = ∞.
AddNonConst(&chained, &negChained, &result)
if !isSameAffinePoint(&result, &infinity) {
if !result.EqualsNonConst(&infinity) {
t.Fatalf("%d: expected point at infinity\ngot (%v, %v, %v)\n", i,
result.X, result.Y, result.Z)
}
Expand All @@ -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.EqualsNonConst(&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.EqualsNonConst(&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)
Expand Down
1 change: 1 addition & 0 deletions dcrec/secp256k1/field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit 22760ba

Please sign in to comment.