lbcd/btcec/pubkey.go
Conner Fromknecht c7d523f83c
btcec/pubkey: optimize decompressPoint using fieldVals
This commit optimizes the decompressPoint subroutine, used in extracting
compressed pubkeys and performing pubkey recovery. We do so by replacing
the use of big.Int.Exp with with square-and-multiply exponentiation of
btcec's more optimized fieldVals, reducing the overall latency and
memory requirements of decompressPoint.

Instead of operating on bits of Q = (P+1)/4, the exponentiation applies
the square-and-multiply operations on full bytes of Q.  Compared to the
original speedup. Compared the bit-wise version, the improvement is
roughly 10%.

A new pair fieldVal methods called Sqrt and SqrtVal are added, which
applies the square-and-multiply exponentiation using precomputed
byte-slice of the value Q.

Comparison against big.Int sqrt and SAM sqrt over bytes of Q:

benchmark                            old ns/op     new ns/op     delta
BenchmarkParseCompressedPubKey-8     35545         23119         -34.96%

benchmark                            old allocs     new allocs     delta
BenchmarkParseCompressedPubKey-8     35             6            -82.86%

benchmark                            old bytes     new bytes     delta
BenchmarkParseCompressedPubKey-8     2777          256           -90.78%
2019-10-02 18:21:59 -07:00

195 lines
5.8 KiB
Go

// Copyright (c) 2013-2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcec
import (
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
)
// These constants define the lengths of serialized public keys.
const (
PubKeyBytesLenCompressed = 33
PubKeyBytesLenUncompressed = 65
PubKeyBytesLenHybrid = 65
)
func isOdd(a *big.Int) bool {
return a.Bit(0) == 1
}
// decompressPoint decompresses a point on the secp256k1 curve given the X point and
// the solution to use.
func decompressPoint(curve *KoblitzCurve, bigX *big.Int, ybit bool) (*big.Int, error) {
var x fieldVal
x.SetByteSlice(bigX.Bytes())
// Compute x^3 + B mod p.
var x3 fieldVal
x3.SquareVal(&x).Mul(&x)
x3.Add(curve.fieldB).Normalize()
// Now calculate sqrt mod p of x^3 + B
// This code used to do a full sqrt based on tonelli/shanks,
// but this was replaced by the algorithms referenced in
// https://bitcointalk.org/index.php?topic=162805.msg1712294#msg1712294
var y fieldVal
y.SqrtVal(&x3)
if ybit != y.IsOdd() {
y.Negate(1)
}
y.Normalize()
// Check that y is a square root of x^3 + B.
var y2 fieldVal
y2.SquareVal(&y).Normalize()
if !y2.Equals(&x3) {
return nil, fmt.Errorf("invalid square root")
}
// Verify that y-coord has expected parity.
if ybit != y.IsOdd() {
return nil, fmt.Errorf("ybit doesn't match oddness")
}
return new(big.Int).SetBytes(y.Bytes()[:]), nil
}
const (
pubkeyCompressed byte = 0x2 // y_bit + x coord
pubkeyUncompressed byte = 0x4 // x coord + y coord
pubkeyHybrid byte = 0x6 // y_bit + x coord + y coord
)
// IsCompressedPubKey returns true the the passed serialized public key has
// been encoded in compressed format, and false otherwise.
func IsCompressedPubKey(pubKey []byte) bool {
// The public key is only compressed if it is the correct length and
// the format (first byte) is one of the compressed pubkey values.
return len(pubKey) == PubKeyBytesLenCompressed &&
(pubKey[0]&^byte(0x1) == pubkeyCompressed)
}
// ParsePubKey parses a public key for a koblitz curve from a bytestring into a
// ecdsa.Publickey, verifying that it is valid. It supports compressed,
// uncompressed and hybrid signature formats.
func ParsePubKey(pubKeyStr []byte, curve *KoblitzCurve) (key *PublicKey, err error) {
pubkey := PublicKey{}
pubkey.Curve = curve
if len(pubKeyStr) == 0 {
return nil, errors.New("pubkey string is empty")
}
format := pubKeyStr[0]
ybit := (format & 0x1) == 0x1
format &= ^byte(0x1)
switch len(pubKeyStr) {
case PubKeyBytesLenUncompressed:
if format != pubkeyUncompressed && format != pubkeyHybrid {
return nil, fmt.Errorf("invalid magic in pubkey str: "+
"%d", pubKeyStr[0])
}
pubkey.X = new(big.Int).SetBytes(pubKeyStr[1:33])
pubkey.Y = new(big.Int).SetBytes(pubKeyStr[33:])
// hybrid keys have extra information, make use of it.
if format == pubkeyHybrid && ybit != isOdd(pubkey.Y) {
return nil, fmt.Errorf("ybit doesn't match oddness")
}
if pubkey.X.Cmp(pubkey.Curve.Params().P) >= 0 {
return nil, fmt.Errorf("pubkey X parameter is >= to P")
}
if pubkey.Y.Cmp(pubkey.Curve.Params().P) >= 0 {
return nil, fmt.Errorf("pubkey Y parameter is >= to P")
}
if !pubkey.Curve.IsOnCurve(pubkey.X, pubkey.Y) {
return nil, fmt.Errorf("pubkey isn't on secp256k1 curve")
}
case PubKeyBytesLenCompressed:
// format is 0x2 | solution, <X coordinate>
// solution determines which solution of the curve we use.
/// y^2 = x^3 + Curve.B
if format != pubkeyCompressed {
return nil, fmt.Errorf("invalid magic in compressed "+
"pubkey string: %d", pubKeyStr[0])
}
pubkey.X = new(big.Int).SetBytes(pubKeyStr[1:33])
pubkey.Y, err = decompressPoint(curve, pubkey.X, ybit)
if err != nil {
return nil, err
}
default: // wrong!
return nil, fmt.Errorf("invalid pub key length %d",
len(pubKeyStr))
}
return &pubkey, nil
}
// PublicKey is an ecdsa.PublicKey with additional functions to
// serialize in uncompressed, compressed, and hybrid formats.
type PublicKey ecdsa.PublicKey
// ToECDSA returns the public key as a *ecdsa.PublicKey.
func (p *PublicKey) ToECDSA() *ecdsa.PublicKey {
return (*ecdsa.PublicKey)(p)
}
// SerializeUncompressed serializes a public key in a 65-byte uncompressed
// format.
func (p *PublicKey) SerializeUncompressed() []byte {
b := make([]byte, 0, PubKeyBytesLenUncompressed)
b = append(b, pubkeyUncompressed)
b = paddedAppend(32, b, p.X.Bytes())
return paddedAppend(32, b, p.Y.Bytes())
}
// SerializeCompressed serializes a public key in a 33-byte compressed format.
func (p *PublicKey) SerializeCompressed() []byte {
b := make([]byte, 0, PubKeyBytesLenCompressed)
format := pubkeyCompressed
if isOdd(p.Y) {
format |= 0x1
}
b = append(b, format)
return paddedAppend(32, b, p.X.Bytes())
}
// SerializeHybrid serializes a public key in a 65-byte hybrid format.
func (p *PublicKey) SerializeHybrid() []byte {
b := make([]byte, 0, PubKeyBytesLenHybrid)
format := pubkeyHybrid
if isOdd(p.Y) {
format |= 0x1
}
b = append(b, format)
b = paddedAppend(32, b, p.X.Bytes())
return paddedAppend(32, b, p.Y.Bytes())
}
// IsEqual compares this PublicKey instance to the one passed, returning true if
// both PublicKeys are equivalent. A PublicKey is equivalent to another, if they
// both have the same X and Y coordinate.
func (p *PublicKey) IsEqual(otherPubKey *PublicKey) bool {
return p.X.Cmp(otherPubKey.X) == 0 &&
p.Y.Cmp(otherPubKey.Y) == 0
}
// paddedAppend appends the src byte slice to dst, returning the new slice.
// If the length of the source is smaller than the passed size, leading zero
// bytes are appended to the dst slice before appending src.
func paddedAppend(size uint, dst, src []byte) []byte {
for i := 0; i < int(size)-len(src); i++ {
dst = append(dst, 0)
}
return append(dst, src...)
}