Skip to content

Commit

Permalink
secp256k1: Expose Jacobian point equivalency func.
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
davecgh committed Sep 6, 2024
1 parent 5a5b031 commit d0e846e
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 4 deletions.
14 changes: 14 additions & 0 deletions dcrec/secp256k1/curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 BenchmarkJacobianPointEquivalency(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
isSameAffinePoint(&point1, &point2)
point1.EquivalentNonConst(&point2)
}
}
272 changes: 269 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("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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
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 d0e846e

Please sign in to comment.