Optimize ScalarBaseMult

Code uses a windowing/precomputing strategy to minimize ECC math.
Every 8-bit window of the 256 bits that compose a possible scalar multiple has a complete map that's pre-computed.
The precomputed data is in secp256k1.go and the generator for that file is in gensecp256k1.go

Also fixed a spelling error in a benchmark test.

Results so far seem to indicate the time taken is about 35% of what it was before.

Closes #2
This commit is contained in:
Jimmy Song 2014-09-24 12:41:07 -05:00
parent 4ca0daacc1
commit d69442834c
5 changed files with 41149 additions and 5 deletions

View file

@ -48,9 +48,9 @@ func BenchmarkAddJacobianNotZOne(b *testing.B) {
} }
} }
// BechmarkScalarBaseMult benchmarks the secp256k1 curve ScalarBaseMult // BenchmarkScalarBaseMult benchmarks the secp256k1 curve ScalarBaseMult
// function. // function.
func BechmarkScalarBaseMult(b *testing.B) { func BenchmarkScalarBaseMult(b *testing.B) {
k := fromHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") k := fromHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575")
curve := btcec.S256() curve := btcec.S256()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View file

@ -37,8 +37,9 @@ var (
// interface from crypto/elliptic. // interface from crypto/elliptic.
type KoblitzCurve struct { type KoblitzCurve struct {
*elliptic.CurveParams *elliptic.CurveParams
q *big.Int q *big.Int
H int // cofactor of the curve. H int // cofactor of the curve.
bytePoints *[32][256][3]fieldVal
} }
// Params returns the parameters for the curve. // Params returns the parameters for the curve.
@ -627,7 +628,28 @@ func (curve *KoblitzCurve) ScalarMult(Bx, By *big.Int, k []byte) (*big.Int, *big
// big endian integer. // big endian integer.
// Part of the elliptic.Curve interface. // Part of the elliptic.Curve interface.
func (curve *KoblitzCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) { func (curve *KoblitzCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) {
return curve.ScalarMult(curve.Gx, curve.Gy, k) // Fall back to slower generic scalar point multiplication when the integer is
// larger than what can be used with the precomputed table which enables
// accelerated multiplication by the known fixed point.
if len(k) > len(curve.bytePoints) {
return curve.ScalarMult(curve.Gx, curve.Gy, k)
}
diff := len(curve.bytePoints) - len(k)
// Point Q = ∞ (point at infinity).
qx, qy, qz := new(fieldVal), new(fieldVal), new(fieldVal)
// curve.bytePoints has all 256 byte points for each 8-bit window. The
// strategy is to add up the byte points. This is best understood by
// expressing k in base-256 which it already sort of is.
// Each "digit" in the 8-bit window can be looked up using bytePoints
// and added together.
for i, byteVal := range k {
point := &curve.bytePoints[diff+i][byteVal]
curve.addJacobian(qx, qy, qz, &point[0], &point[1], &point[2], qx, qy, qz)
}
return curve.fieldJacobianToBigAffine(qx, qy, qz)
} }
// QPlus1Div4 returns the Q+1/4 constant for the curve for use in calculating // QPlus1Div4 returns the Q+1/4 constant for the curve for use in calculating
@ -656,6 +678,7 @@ func initS256() {
secp256k1.H = 1 secp256k1.H = 1
secp256k1.q = new(big.Int).Div(new(big.Int).Add(secp256k1.P, secp256k1.q = new(big.Int).Div(new(big.Int).Add(secp256k1.P,
big.NewInt(1)), big.NewInt(4)) big.NewInt(1)), big.NewInt(4))
secp256k1.bytePoints = &secp256k1BytePoints
} }
// S256 returns a Curve which implements secp256k1. // S256 returns a Curve which implements secp256k1.

View file

@ -555,6 +555,28 @@ func TestBaseMult(t *testing.T) {
} }
} }
func TestBaseMultVerify(t *testing.T) {
s256 := btcec.S256()
for bytes := 1; bytes < 35; bytes++ {
for i := 0; i < 30; i++ {
data := make([]byte, bytes)
_, err := rand.Read(data)
if err != nil {
t.Errorf("failed to read random data for %s", i)
continue
}
x, y := s256.ScalarBaseMult(data)
xWant, yWant := s256.ScalarMult(s256.Gx, s256.Gy, data)
if x.Cmp(xWant) != 0 || y.Cmp(yWant) != 0 {
t.Errorf("%d: bad output for %X: got (%X, %X), want (%X, %X)", i, data, x, y, xWant, yWant)
}
if testing.Short() && i > 2 {
break
}
}
}
}
//TODO: test more curves? //TODO: test more curves?
func BenchmarkBaseMult(b *testing.B) { func BenchmarkBaseMult(b *testing.B) {
b.ResetTimer() b.ResetTimer()

66
gensecp256k1.go Normal file
View file

@ -0,0 +1,66 @@
// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// +build gensecp256k1
package btcec
import (
"fmt"
)
// getDoublingPoints returns all the possible G^(2^i) for i in
// 0..n-1 where n is the curve's bit size (256 in the case of secp256k1)
// the coordinates are recorded as Jacobian coordinates.
func (curve *KoblitzCurve) getDoublingPoints() [][3]fieldVal {
bitSize := curve.Params().BitSize
doublingPoints := make([][3]fieldVal, bitSize)
// initialize px, py, pz to the Jacobian coordinates for the base point
px, py := curve.bigAffineToField(curve.Gx, curve.Gy)
pz := new(fieldVal).SetInt(1)
for i := 0; i < bitSize; i++ {
doublingPoints[i] = [3]fieldVal{*px, *py, *pz}
// P = 2*P
curve.doubleJacobian(px, py, pz, px, py, pz)
}
return doublingPoints
}
// PrintBytePoints prints all the possible points per 8-bit window.
// normally, this is used to generate secp256k1.go
func (curve *KoblitzCurve) PrintBytePoints() {
bitSize := curve.Params().BitSize
byteSize := bitSize / 8
doublingPoints := curve.getDoublingPoints()
fmt.Println("// Copyright (c) 2014 Conformal Systems LLC.")
fmt.Println("// Use of this source code is governed by an ISC")
fmt.Println("// license that can be found in the LICENSE file.")
fmt.Println()
fmt.Println("package btcec")
fmt.Println()
fmt.Println("// Auto-generated file (see gensecp256k1.go)")
fmt.Printf("var secp256k1BytePoints = [%d][256][3]fieldVal{\n", byteSize)
// Segregate the bits into byte-sized windows
for byteNum := 0; byteNum < byteSize; byteNum++ {
fmt.Printf("\t{\n")
// grab the 8 bits that make up this byte from doublingPoints
startingBit := 8 * (byteSize - byteNum - 1)
computingPoints := doublingPoints[startingBit : startingBit+8]
// compute all points in this window
for i := 0; i < 256; i++ {
px, py, pz := new(fieldVal), new(fieldVal), new(fieldVal)
for j := 0; j < 8; j++ {
if i>>uint(j)&1 == 1 {
curve.addJacobian(px, py, pz, &computingPoints[j][0],
&computingPoints[j][1], &computingPoints[j][2], px, py, pz)
}
}
fmt.Printf("\t\t{\n\t\t\tfieldVal{[10]uint32{%d, %d, %d, %d, %d, %d, %d, %d, %d, %d}},\n", px.n[0], px.n[1], px.n[2], px.n[3], px.n[4], px.n[5], px.n[6], px.n[7], px.n[8], px.n[9])
fmt.Printf("\t\t\tfieldVal{[10]uint32{%d, %d, %d, %d, %d, %d, %d, %d, %d, %d}},\n", py.n[0], py.n[1], py.n[2], py.n[3], py.n[4], py.n[5], py.n[6], py.n[7], py.n[8], py.n[9])
fmt.Printf("\t\t\tfieldVal{[10]uint32{%d, %d, %d, %d, %d, %d, %d, %d, %d, %d}},\n\t\t},\n", pz.n[0], pz.n[1], pz.n[2], pz.n[3], pz.n[4], pz.n[5], pz.n[6], pz.n[7], pz.n[8], pz.n[9])
}
fmt.Printf("\t},\n")
}
fmt.Printf("}\n")
}

41033
secp256k1.go Normal file

File diff suppressed because it is too large Load diff