Add code to produce and verify compact signatures.
The format used is identical to that used in bitcoind.
This commit is contained in:
parent
218906a91e
commit
ff3fac426d
5 changed files with 297 additions and 41 deletions
2
btcec.go
2
btcec.go
|
@ -38,6 +38,7 @@ var (
|
|||
type KoblitzCurve struct {
|
||||
*elliptic.CurveParams
|
||||
q *big.Int
|
||||
H int // cofactor of the curve.
|
||||
}
|
||||
|
||||
// Params returns the parameters for the curve.
|
||||
|
@ -652,6 +653,7 @@ func initS256() {
|
|||
secp256k1.Gx, _ = new(big.Int).SetString("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16)
|
||||
secp256k1.Gy, _ = new(big.Int).SetString("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)
|
||||
secp256k1.BitSize = 256
|
||||
secp256k1.H = 1
|
||||
secp256k1.q = new(big.Int).Div(new(big.Int).Add(secp256k1.P,
|
||||
big.NewInt(1)), big.NewInt(4))
|
||||
}
|
||||
|
|
47
pubkey.go
47
pubkey.go
|
@ -14,6 +14,32 @@ func isOdd(a *big.Int) bool {
|
|||
return a.Bit(0) == 1
|
||||
}
|
||||
|
||||
// decompressPoint decompresses a point on the given curve given the X point and
|
||||
// the solution to use.
|
||||
func decompressPoint(curve *KoblitzCurve, x *big.Int, ybit bool) (*big.Int, error) {
|
||||
// TODO(oga) This will probably only work for secp256k1 due to
|
||||
// optimisations.
|
||||
|
||||
// Y = +-sqrt(x^3 + B)
|
||||
x3 := new(big.Int).Mul(x, x)
|
||||
x3.Mul(x3, x)
|
||||
x3.Add(x3, curve.Params().B)
|
||||
|
||||
// now calculate sqrt mod p of x2 + 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
|
||||
y := new(big.Int).Exp(x3, curve.QPlus1Div4(), curve.Params().P)
|
||||
|
||||
if ybit != isOdd(y) {
|
||||
y.Sub(curve.Params().P, y)
|
||||
}
|
||||
if ybit != isOdd(y) {
|
||||
return nil, fmt.Errorf("ybit doesn't match oddness")
|
||||
}
|
||||
return y, nil
|
||||
}
|
||||
|
||||
const (
|
||||
pubkeyCompressed byte = 0x2 // y_bit + x coord
|
||||
pubkeyUncompressed byte = 0x4 // x coord + y coord
|
||||
|
@ -53,25 +79,10 @@ func ParsePubKey(pubKeyStr []byte, curve *KoblitzCurve) (key *ecdsa.PublicKey, e
|
|||
"pubkey string: %d", pubKeyStr[0])
|
||||
}
|
||||
pubkey.X = new(big.Int).SetBytes(pubKeyStr[1:33])
|
||||
// Y = +-sqrt(x^3 + B)
|
||||
x3 := new(big.Int).Mul(pubkey.X, pubkey.X)
|
||||
x3.Mul(x3, pubkey.X)
|
||||
x3.Add(x3, pubkey.Curve.Params().B)
|
||||
|
||||
// now calculate sqrt mod p of x2 + 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
|
||||
y := new(big.Int).Exp(x3, curve.QPlus1Div4(), pubkey.Curve.Params().P)
|
||||
|
||||
if ybit != isOdd(y) {
|
||||
y.Sub(pubkey.Curve.Params().P, y)
|
||||
pubkey.Y, err = decompressPoint(curve, pubkey.X, ybit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ybit != isOdd(y) {
|
||||
return nil, fmt.Errorf("ybit doesn't match oddness")
|
||||
}
|
||||
|
||||
pubkey.Y = y
|
||||
default: // wrong!
|
||||
return nil, fmt.Errorf("invalid pub key length %d",
|
||||
len(pubKeyStr))
|
||||
|
|
168
signature.go
168
signature.go
|
@ -5,7 +5,9 @@
|
|||
package btcec
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
@ -213,3 +215,169 @@ func canonicalPadding(b []byte) error {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// hashToInt converts a hash value to an integer. There is some disagreement
|
||||
// about how this is done. [NSA] suggests that this is done in the obvious
|
||||
// manner, but [SECG] truncates the hash to the bit-length of the curve order
|
||||
// first. We follow [SECG] because that's what OpenSSL does. Additionally,
|
||||
// OpenSSL right shifts excess bits from the number if the hash is too large
|
||||
// and we mirror that too.
|
||||
// This is borrowed from crypto/ecdsa.
|
||||
func hashToInt(hash []byte, c elliptic.Curve) *big.Int {
|
||||
orderBits := c.Params().N.BitLen()
|
||||
orderBytes := (orderBits + 7) / 8
|
||||
if len(hash) > orderBytes {
|
||||
hash = hash[:orderBytes]
|
||||
}
|
||||
|
||||
ret := new(big.Int).SetBytes(hash)
|
||||
excess := len(hash)*8 - orderBits
|
||||
if excess > 0 {
|
||||
ret.Rsh(ret, uint(excess))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// recoverKeyFromSignature recoves a public key from the signature "sig" on the
|
||||
// given message hash "msg". Based on the algorithm found in section 5.1.5 of
|
||||
// SEC 1 Ver 2.0, page 47-48 (53 and 54 in the pdf). This performs the details
|
||||
// in the inner loop in Step 1. The counter provided is actually the j parameter
|
||||
// of the loop * 2 - on the first iteration of j we do the R case, else the -R
|
||||
// case in step 1.6. This counter is used in the bitcoin compressed signature
|
||||
// format and thus we match bitcoind's behaviour here.
|
||||
func recoverKeyFromSignature(curve *KoblitzCurve, sig *Signature, msg []byte,
|
||||
iter int, doChecks bool) (*ecdsa.PublicKey, error) {
|
||||
// 1.1 x = (n * i) + r
|
||||
Rx := new(big.Int).Mul(curve.Params().N,
|
||||
new(big.Int).SetInt64(int64(iter/2)))
|
||||
Rx.Add(Rx, sig.R)
|
||||
if Rx.Cmp(curve.Params().P) != -1 {
|
||||
return nil, errors.New("calculated Rx is larger than curve P")
|
||||
}
|
||||
|
||||
// convert 02<Rx> to point R. (step 1.2 and 1.3). If we are on an odd
|
||||
// iteration then 1.6 will be done with -R, so we calculate the other
|
||||
// term when uncompressing the point.
|
||||
Ry, err := decompressPoint(curve, Rx, iter%2 == 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1.4 Check n*R is point at infinity
|
||||
if doChecks {
|
||||
nRx, nRy := curve.ScalarMult(Rx, Ry, curve.Params().N.Bytes())
|
||||
if nRx.Sign() != 0 || nRy.Sign() != 0 {
|
||||
return nil, errors.New("R*n does not equal the point at infinity")
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5 calculate e from message using the same algorithm as ecdsa
|
||||
// signature calculation.
|
||||
e := hashToInt(msg, curve)
|
||||
|
||||
// Step 1.6.1:
|
||||
// We calculate the two terms sR and eG separately multiplied by the
|
||||
// inverse of r (from the signature). We then add them to calculate
|
||||
// Q = r^-1(sR-eG)
|
||||
invr := new(big.Int).ModInverse(sig.R, curve.Params().N)
|
||||
|
||||
// first term.
|
||||
invrS := new(big.Int).Mul(invr, sig.S)
|
||||
invrS.Mod(invrS, curve.Params().N)
|
||||
sRx, sRy := curve.ScalarMult(Rx, Ry, invrS.Bytes())
|
||||
|
||||
// second term.
|
||||
e.Neg(e)
|
||||
e.Mod(e, curve.Params().N)
|
||||
e.Mul(e, invr)
|
||||
e.Mod(e, curve.Params().N)
|
||||
minuseGx, minuseGy := curve.ScalarBaseMult(e.Bytes())
|
||||
|
||||
// TODO(oga) this would be faster if we did a mult and add in one
|
||||
// step to prevent the jacobian conversion back and forth.
|
||||
Qx, Qy := curve.Add(sRx, sRy, minuseGx, minuseGy)
|
||||
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: Qx,
|
||||
Y: Qy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignCompact produces a compact signature of the data in hash with the given
|
||||
// private key on the given koblitz curve. The isCompressed parameter should
|
||||
// be used to detail if the given signature should reference a compressed
|
||||
// public key or not. If successful the bytes of the compact signature will be
|
||||
// returned in the format:
|
||||
// <(byte of 27+public key solution)+4 if compressed >< padded bytes for signature R><padded bytes for signature S>
|
||||
// where the R and S parameters are padde up to the bitlengh of the curve.
|
||||
func SignCompact(curve *KoblitzCurve, key *ecdsa.PrivateKey,
|
||||
hash []byte, isCompressedKey bool) ([]byte, error) {
|
||||
r, s, err := ecdsa.Sign(rand.Reader, key, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := &Signature{R: r, S: s}
|
||||
// bitcoind checks the bit length of R and S here. The ecdsa signature
|
||||
// algorithm returns R and S mod N therefore they will be the bitsize of
|
||||
// the curve, and thus correctly sized.
|
||||
for i := 0; i < (curve.H+1)*2; i++ {
|
||||
pk, err := recoverKeyFromSignature(curve, sig, hash, i, true)
|
||||
if err == nil && pk.X.Cmp(key.X) == 0 && pk.Y.Cmp(key.Y) == 0 {
|
||||
result := make([]byte, 1, 2*(curve.BitSize/8)+1)
|
||||
result[0] = 27 + byte(i)
|
||||
if isCompressedKey {
|
||||
result[0] += 4
|
||||
}
|
||||
// Not sure this needs rounding but safer to do so.
|
||||
curvelen := (curve.BitSize + 7) / 8
|
||||
|
||||
// Pad R and S to curvelen if needed.
|
||||
bytelen := (sig.R.BitLen() + 7) / 8
|
||||
if bytelen < curvelen {
|
||||
result = append(result,
|
||||
make([]byte, curvelen-bytelen)...)
|
||||
}
|
||||
result = append(result, sig.R.Bytes()...)
|
||||
|
||||
bytelen = (sig.S.BitLen() + 7) / 8
|
||||
if bytelen < curvelen {
|
||||
result = append(result,
|
||||
make([]byte, curvelen-bytelen)...)
|
||||
}
|
||||
result = append(result, sig.S.Bytes()...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("no valid solution for pubkey found")
|
||||
}
|
||||
|
||||
// RecoverCompact verifies the compact signature "signature" of "hash" for the
|
||||
// Koblitz curve in "curve". If the signature matches then the recovered public
|
||||
// key will be returned as well as a boolen if the original key was compressed
|
||||
// or not, else an error will be returned.
|
||||
func RecoverCompact(curve *KoblitzCurve, signature,
|
||||
hash []byte) (*ecdsa.PublicKey, bool, error) {
|
||||
bitlen := (curve.BitSize + 7) / 8
|
||||
if len(signature) != 1+bitlen*2 {
|
||||
return nil, false, errors.New("invalid compact signature size")
|
||||
}
|
||||
|
||||
iteration := int((signature[0] - 27) & ^byte(4))
|
||||
|
||||
// format is <header byte><bitlen R><bitlen S>
|
||||
sig := &Signature{
|
||||
R: new(big.Int).SetBytes(signature[1 : bitlen+1]),
|
||||
S: new(big.Int).SetBytes(signature[bitlen+1:]),
|
||||
}
|
||||
// The iteration used here was encoded
|
||||
key, err := recoverKeyFromSignature(curve, sig, hash, iteration, false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return key, ((signature[0] - 27) & 4) == 4, nil
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ package btcec_test
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/conformal/btcec"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
@ -420,3 +423,70 @@ func TestSignatureSerialize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSignCompact(t *testing.T, tag string, curve *btcec.KoblitzCurve,
|
||||
data []byte, isCompressed bool) {
|
||||
priv, _ := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
|
||||
hashed := []byte("testing")
|
||||
sig, err := btcec.SignCompact(curve, priv, hashed, isCompressed)
|
||||
if err != nil {
|
||||
t.Errorf("%s: error signing: %s", tag, err)
|
||||
return
|
||||
}
|
||||
|
||||
pk, wasCompressed, err := btcec.RecoverCompact(curve, sig, hashed)
|
||||
if err != nil {
|
||||
t.Errorf("%s: error recovering: %s", tag, err)
|
||||
return
|
||||
}
|
||||
if pk.X.Cmp(priv.X) != 0 || pk.Y.Cmp(priv.Y) != 0 {
|
||||
t.Errorf("%s: recovered pubkey doesn't match original "+
|
||||
"(%v,%v) vs (%v,%v) ", tag, pk.X, pk.Y, priv.X, priv.Y)
|
||||
return
|
||||
}
|
||||
if wasCompressed != isCompressed {
|
||||
t.Errorf("%s: recovered pubkey doesn't match compressed state "+
|
||||
"(%v vs %v)", tag, isCompressed, wasCompressed)
|
||||
return
|
||||
}
|
||||
|
||||
// If we change the compressed bit we should get the same key back,
|
||||
// but the compressed flag should be reversed.
|
||||
if isCompressed {
|
||||
sig[0] -= 4
|
||||
} else {
|
||||
sig[0] += 4
|
||||
}
|
||||
|
||||
pk, wasCompressed, err = btcec.RecoverCompact(curve, sig, hashed)
|
||||
if err != nil {
|
||||
t.Errorf("%s: error recovering (2): %s", tag, err)
|
||||
return
|
||||
}
|
||||
if pk.X.Cmp(priv.X) != 0 || pk.Y.Cmp(priv.Y) != 0 {
|
||||
t.Errorf("%s: recovered pubkey (2) doesn't match original "+
|
||||
"(%v,%v) vs (%v,%v) ", tag, pk.X, pk.Y, priv.X, priv.Y)
|
||||
return
|
||||
}
|
||||
if wasCompressed == isCompressed {
|
||||
t.Errorf("%s: recovered pubkey doesn't match reversed "+
|
||||
"compressed state (%v vs %v)", tag, isCompressed,
|
||||
wasCompressed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignCompact(t *testing.T) {
|
||||
for i := 0; i < 256; i++ {
|
||||
name := fmt.Sprintf("test %d", i)
|
||||
data := make([]byte, 32)
|
||||
_, err := rand.Read(data)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read random data for %s", name)
|
||||
continue
|
||||
}
|
||||
compressed := i%2 != 0
|
||||
testSignCompact(t, name, btcec.S256(), data, compressed)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,50 +10,55 @@ github.com/conformal/btcec/field.go fieldVal.PutBytes 100.00% (32/32)
|
|||
github.com/conformal/btcec/btcec.go KoblitzCurve.addZ1EqualsZ2 100.00% (30/30)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.addZ1AndZ2EqualsOne 100.00% (29/29)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.addJacobian 100.00% (22/22)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.doubleZ1EqualsOne 100.00% (18/18)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.doubleGeneric 100.00% (18/18)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.doubleZ1EqualsOne 100.00% (18/18)
|
||||
github.com/conformal/btcec/signature.go Signature.Serialize 100.00% (13/13)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.fieldJacobianToBigAffine 100.00% (12/12)
|
||||
github.com/conformal/btcec/field.go fieldVal.MulInt 100.00% (12/12)
|
||||
github.com/conformal/btcec/field.go fieldVal.Add2 100.00% (11/11)
|
||||
github.com/conformal/btcec/field.go fieldVal.Add 100.00% (11/11)
|
||||
github.com/conformal/btcec/field.go fieldVal.NegateVal 100.00% (11/11)
|
||||
github.com/conformal/btcec/field.go fieldVal.Add2 100.00% (11/11)
|
||||
github.com/conformal/btcec/field.go fieldVal.SetBytes 100.00% (11/11)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarMult 100.00% (10/10)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.Add 100.00% (10/10)
|
||||
github.com/conformal/btcec/field.go fieldVal.NegateVal 100.00% (11/11)
|
||||
github.com/conformal/btcec/field.go fieldVal.Zero 100.00% (10/10)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.Add 100.00% (10/10)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarMult 100.00% (10/10)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.doubleJacobian 100.00% (9/9)
|
||||
github.com/conformal/btcec/signature.go canonicalizeInt 100.00% (8/8)
|
||||
github.com/conformal/btcec/btcec.go initS256 100.00% (9/9)
|
||||
github.com/conformal/btcec/pubkey.go PublicKey.SerializeHybrid 100.00% (8/8)
|
||||
github.com/conformal/btcec/btcec.go initS256 100.00% (8/8)
|
||||
github.com/conformal/btcec/signature.go canonicalizeInt 100.00% (8/8)
|
||||
github.com/conformal/btcec/pubkey.go PublicKey.SerializeCompressed 100.00% (7/7)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.Double 100.00% (7/7)
|
||||
github.com/conformal/btcec/field.go fieldVal.SetByteSlice 100.00% (5/5)
|
||||
github.com/conformal/btcec/pubkey.go PublicKey.SerializeUncompressed 100.00% (5/5)
|
||||
github.com/conformal/btcec/field.go fieldVal.SetByteSlice 100.00% (5/5)
|
||||
github.com/conformal/btcec/pubkey.go pad 100.00% (5/5)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.bigAffineToField 100.00% (4/4)
|
||||
github.com/conformal/btcec/field.go fieldVal.SetHex 100.00% (4/4)
|
||||
github.com/conformal/btcec/signature.go canonicalPadding 100.00% (4/4)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.bigAffineToField 100.00% (4/4)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.IsOnCurve 100.00% (4/4)
|
||||
github.com/conformal/btcec/field.go fieldVal.SetInt 100.00% (3/3)
|
||||
github.com/conformal/btcec/field.go fieldVal.Bytes 100.00% (3/3)
|
||||
github.com/conformal/btcec/field.go fieldVal.IsZero 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.AddInt 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.Equals 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.Set 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.String 100.00% (2/2)
|
||||
github.com/conformal/btcec/btcec.go S256 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.IsZero 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.Equals 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.AddInt 100.00% (2/2)
|
||||
github.com/conformal/btcec/field.go fieldVal.Set 100.00% (2/2)
|
||||
github.com/conformal/btcec/pubkey.go isOdd 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.Mul 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.Square 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.Params 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarBaseMult 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.QPlus1Div4 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go initAll 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.Negate 100.00% (1/1)
|
||||
github.com/conformal/btcec/signature.go ParseSignature 100.00% (1/1)
|
||||
github.com/conformal/btcec/signature.go ParseDERSignature 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarBaseMult 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.Mul 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.Params 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.Square 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.Negate 100.00% (1/1)
|
||||
github.com/conformal/btcec/field.go fieldVal.IsOdd 100.00% (1/1)
|
||||
github.com/conformal/btcec/pubkey.go ParsePubKey 96.88% (31/32)
|
||||
github.com/conformal/btcec ------------------------------------- 99.88% (846/847)
|
||||
github.com/conformal/btcec/btcec.go initAll 100.00% (1/1)
|
||||
github.com/conformal/btcec/btcec.go KoblitzCurve.QPlus1Div4 100.00% (1/1)
|
||||
github.com/conformal/btcec/signature.go ParseSignature 100.00% (1/1)
|
||||
github.com/conformal/btcec/pubkey.go ParsePubKey 96.15% (25/26)
|
||||
github.com/conformal/btcec/signature.go SignCompact 90.91% (20/22)
|
||||
github.com/conformal/btcec/pubkey.go decompressPoint 88.89% (8/9)
|
||||
github.com/conformal/btcec/signature.go recoverKeyFromSignature 86.96% (20/23)
|
||||
github.com/conformal/btcec/signature.go RecoverCompact 77.78% (7/9)
|
||||
github.com/conformal/btcec/signature.go hashToInt 77.78% (7/9)
|
||||
github.com/conformal/btcec ------------------------------------- 98.80% (903/914)
|
||||
|
||||
|
|
Loading…
Reference in a new issue