Skip to content

Commit

Permalink
btcec: move new btcec module into v2 sub-folder
Browse files Browse the repository at this point in the history
In this commit, we modify our approach slightly and move the new `btcec`
module into the a v2 sub-folder. Keeping the prior commits allows us to
keep track of the core diff here more easily.

We need to do this as due to the way the current modules and btcutil are
set up, we can't switch over to the new module rigth away. Instead,
we'll need to first update btcutil to use the new module, _then_
switch things over.
  • Loading branch information
Roasbeef committed Dec 2, 2021
1 parent a2cb4b5 commit d604973
Show file tree
Hide file tree
Showing 38 changed files with 8,606 additions and 1,212 deletions.
298 changes: 38 additions & 260 deletions btcec/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,114 +6,43 @@ package btcec

import (
"encoding/hex"
"math/big"
"testing"

secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
)

// setHex decodes the passed big-endian hex string into the internal field value
// representation. Only the first 32-bytes are used.
//
// This is NOT constant time.
//
// The field value is returned to support chaining. This enables syntax like:
// f := new(FieldVal).SetHex("0abc").Add(1) so that f = 0x0abc + 1
func setHex(hexString string) *FieldVal {
if len(hexString)%2 != 0 {
hexString = "0" + hexString
}
bytes, _ := hex.DecodeString(hexString)

var f FieldVal
f.SetByteSlice(bytes)

return &f
}

// hexToFieldVal converts the passed hex string into a FieldVal and will panic
// if there is an error. This is only provided for the hard-coded constants so
// errors in the source code can be detected. It will only (and must only) be
// called with hard-coded values.
func hexToFieldVal(s string) *FieldVal {
b, err := hex.DecodeString(s)
if err != nil {
panic("invalid hex in source file: " + s)
}
var f FieldVal
if overflow := f.SetByteSlice(b); overflow {
panic("hex in source file overflows mod P: " + s)
}
return &f
}

// fromHex converts the passed hex string into a big integer pointer and will
// panic is there is an error. This is only provided for the hard-coded
// constants so errors in the source code can bet detected. It will only (and
// must only) be called for initialization purposes.
func fromHex(s string) *big.Int {
if s == "" {
return big.NewInt(0)
}
r, ok := new(big.Int).SetString(s, 16)
if !ok {
panic("invalid hex in source file: " + s)
}
return r
}

// 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.
func jacobianPointFromHex(x, y, z string) JacobianPoint {
var p JacobianPoint
p.X = *setHex(x)
p.Y = *setHex(y)
p.Z = *setHex(z)

return p
}

// BenchmarkAddNonConst benchmarks the secp256k1 curve AddNonConst function with
// BenchmarkAddJacobian benchmarks the secp256k1 curve addJacobian function with
// Z values of 1 so that the associated optimizations are used.
func BenchmarkAddJacobian(b *testing.B) {
p1 := jacobianPointFromHex(
"34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6",
"0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232",
"1",
)
p2 := jacobianPointFromHex(
"34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6",
"0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232",
"1",
)

b.ReportAllocs()
b.ResetTimer()
var result JacobianPoint
b.StopTimer()
x1 := new(fieldVal).SetHex("34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6")
y1 := new(fieldVal).SetHex("0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232")
z1 := new(fieldVal).SetHex("1")
x2 := new(fieldVal).SetHex("34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6")
y2 := new(fieldVal).SetHex("0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232")
z2 := new(fieldVal).SetHex("1")
x3, y3, z3 := new(fieldVal), new(fieldVal), new(fieldVal)
curve := S256()
b.StartTimer()
for i := 0; i < b.N; i++ {
secp.AddNonConst(&p1, &p2, &result)
curve.addJacobian(x1, y1, z1, x2, y2, z2, x3, y3, z3)
}
}

// BenchmarkAddNonConstNotZOne benchmarks the secp256k1 curve AddNonConst
// BenchmarkAddJacobianNotZOne benchmarks the secp256k1 curve addJacobian
// function with Z values other than one so the optimizations associated with
// Z=1 aren't used.
func BenchmarkAddJacobianNotZOne(b *testing.B) {
x1 := setHex("d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718")
y1 := setHex("5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190")
z1 := setHex("2")
x2 := setHex("91abba6a34b7481d922a4bd6a04899d5a686f6cf6da4e66a0cb427fb25c04bd4")
y2 := setHex("03fede65e30b4e7576a2abefc963ddbf9fdccbf791b77c29beadefe49951f7d1")
z2 := setHex("3")
p1 := MakeJacobianPoint(x1, y1, z1)
p2 := MakeJacobianPoint(x2, y2, z2)

b.ReportAllocs()
b.ResetTimer()
var result JacobianPoint
b.StopTimer()
x1 := new(fieldVal).SetHex("d3e5183c393c20e4f464acf144ce9ae8266a82b67f553af33eb37e88e7fd2718")
y1 := new(fieldVal).SetHex("5b8f54deb987ec491fb692d3d48f3eebb9454b034365ad480dda0cf079651190")
z1 := new(fieldVal).SetHex("2")
x2 := new(fieldVal).SetHex("91abba6a34b7481d922a4bd6a04899d5a686f6cf6da4e66a0cb427fb25c04bd4")
y2 := new(fieldVal).SetHex("03fede65e30b4e7576a2abefc963ddbf9fdccbf791b77c29beadefe49951f7d1")
z2 := new(fieldVal).SetHex("3")
x3, y3, z3 := new(fieldVal), new(fieldVal), new(fieldVal)
curve := S256()
b.StartTimer()
for i := 0; i < b.N; i++ {
AddNonConst(&p1, &p2, &result)
curve.addJacobian(x1, y1, z1, x2, y2, z2, x3, y3, z3)
}
}

Expand Down Expand Up @@ -148,200 +77,49 @@ func BenchmarkScalarMult(b *testing.B) {
}
}

// nafScalar represents a positive integer up to a maximum value of 2^256 - 1
// encoded in non-adjacent form.
//
// NAF is a signed-digit representation where each digit can be +1, 0, or -1.
//
// In order to efficiently encode that information, this type uses two arrays, a
// "positive" array where set bits represent the +1 signed digits and a
// "negative" array where set bits represent the -1 signed digits. 0 is
// represented by neither array having a bit set in that position.
//
// The Pos and Neg methods return the aforementioned positive and negative
// arrays, respectively.
type nafScalar struct {
// pos houses the positive portion of the representation. An additional
// byte is required for the positive portion because the NAF encoding can be
// up to 1 bit longer than the normal binary encoding of the value.
//
// neg houses the negative portion of the representation. Even though the
// additional byte is not required for the negative portion, since it can
// never exceed the length of the normal binary encoding of the value,
// keeping the same length for positive and negative portions simplifies
// working with the representation and allows extra conditional branches to
// be avoided.
//
// start and end specify the starting and ending index to use within the pos
// and neg arrays, respectively. This allows fixed size arrays to be used
// versus needing to dynamically allocate space on the heap.
//
// NOTE: The fields are defined in the order that they are to minimize the
// padding on 32-bit and 64-bit platforms.
pos [33]byte
start, end uint8
neg [33]byte
}

// Pos returns the bytes of the encoded value with bits set in the positions
// that represent a signed digit of +1.
func (s *nafScalar) Pos() []byte {
return s.pos[s.start:s.end]
}

// Neg returns the bytes of the encoded value with bits set in the positions
// that represent a signed digit of -1.
func (s *nafScalar) Neg() []byte {
return s.neg[s.start:s.end]
}

// naf takes a positive integer up to a maximum value of 2^256 - 1 and returns
// its non-adjacent form (NAF), which is a unique signed-digit representation
// such that no two consecutive digits are nonzero. See the documentation for
// the returned type for details on how the representation is encoded
// efficiently and how to interpret it
//
// NAF is useful in that it has the fewest nonzero digits of any signed digit
// representation, only 1/3rd of its digits are nonzero on average, and at least
// half of the digits will be 0.
//
// The aforementioned properties are particularly beneficial for optimizing
// elliptic curve point multiplication because they effectively minimize the
// number of required point additions in exchange for needing to perform a mix
// of fewer point additions and subtractions and possibly one additional point
// doubling. This is an excellent tradeoff because subtraction of points has
// the same computational complexity as addition of points and point doubling is
// faster than both.
func naf(k []byte) nafScalar {
// Strip leading zero bytes.
for len(k) > 0 && k[0] == 0x00 {
k = k[1:]
}

// The non-adjacent form (NAF) of a positive integer k is an expression
// k = ∑_(i=0, l-1) k_i * 2^i where k_i ∈ {0,±1}, k_(l-1) != 0, and no two
// consecutive digits k_i are nonzero.
//
// The traditional method of computing the NAF of a positive integer is
// given by algorithm 3.30 in [GECC]. It consists of repeatedly dividing k
// by 2 and choosing the remainder so that the quotient (k−r)/2 is even
// which ensures the next NAF digit is 0. This requires log_2(k) steps.
//
// However, in [BRID], Prodinger notes that a closed form expression for the
// NAF representation is the bitwise difference 3k/2 - k/2. This is more
// efficient as it can be computed in O(1) versus the O(log(n)) of the
// traditional approach.
//
// The following code makes use of that formula to compute the NAF more
// efficiently.
//
// To understand the logic here, observe that the only way the NAF has a
// nonzero digit at a given bit is when either 3k/2 or k/2 has a bit set in
// that position, but not both. In other words, the result of a bitwise
// xor. This can be seen simply by considering that when the bits are the
// same, the subtraction is either 0-0 or 1-1, both of which are 0.
//
// Further, observe that the "+1" digits in the result are contributed by
// 3k/2 while the "-1" digits are from k/2. So, they can be determined by
// taking the bitwise and of each respective value with the result of the
// xor which identifies which bits are nonzero.
//
// Using that information, this loops backwards from the least significant
// byte to the most significant byte while performing the aforementioned
// calculations by propagating the potential carry and high order bit from
// the next word during the right shift.
kLen := len(k)
var result nafScalar
var carry uint8
for byteNum := kLen - 1; byteNum >= 0; byteNum-- {
// Calculate k/2. Notice the carry from the previous word is added and
// the low order bit from the next word is shifted in accordingly.
kc := uint16(k[byteNum]) + uint16(carry)
var nextWord uint8
if byteNum > 0 {
nextWord = k[byteNum-1]
}
halfK := kc>>1 | uint16(nextWord<<7)

// Calculate 3k/2 and determine the non-zero digits in the result.
threeHalfK := kc + halfK
nonZeroResultDigits := threeHalfK ^ halfK

// Determine the signed digits {0, ±1}.
result.pos[byteNum+1] = uint8(threeHalfK & nonZeroResultDigits)
result.neg[byteNum+1] = uint8(halfK & nonZeroResultDigits)

// Propagate the potential carry from the 3k/2 calculation.
carry = uint8(threeHalfK >> 8)
}
result.pos[0] = carry

// Set the starting and ending positions within the fixed size arrays to
// identify the bytes that are actually used. This is important since the
// encoding is big endian and thus trailing zero bytes changes its value.
result.start = 1 - carry
result.end = uint8(kLen + 1)
return result
}

// BenchmarkNAF benchmarks the NAF function.
func BenchmarkNAF(b *testing.B) {
k := fromHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575")
for i := 0; i < b.N; i++ {
naf(k.Bytes())
NAF(k.Bytes())
}
}

// hexToModNScalar converts the passed hex string into a ModNScalar and will
// panic if there is an error. This is only provided for the hard-coded
// constants so errors in the source code can be detected. It will only (and
// must only) be called with hard-coded values.
func hexToModNScalar(s string) *ModNScalar {
b, err := hex.DecodeString(s)
if err != nil {
panic("invalid hex in source file: " + s)
}
var scalar ModNScalar
if overflow := scalar.SetByteSlice(b); overflow {
panic("hex in source file overflows mod N scalar: " + s)
}
return &scalar
}

// BenchmarkSigVerify benchmarks how long it takes the secp256k1 curve to
// verify signatures.
func BenchmarkSigVerify(b *testing.B) {
b.StopTimer()
// Randomly generated keypair.
// Private key: 9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d
pubKey := NewPublicKey(
hexToFieldVal("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab"),
hexToFieldVal("ab65528eefbb8057aa85d597258a3fbd481a24633bc9b47a9aa045c91371de52"),
)
pubKey := PublicKey{
Curve: S256(),
X: fromHex("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab"),
Y: fromHex("ab65528eefbb8057aa85d597258a3fbd481a24633bc9b47a9aa045c91371de52"),
}

// Double sha256 of []byte{0x01, 0x02, 0x03, 0x04}
msgHash := fromHex("8de472e2399610baaa7f84840547cd409434e31f5d3bd71e4d947f283874f9c0")
sig := NewSignature(
hexToModNScalar("fef45d2892953aa5bbcdb057b5e98b208f1617a7498af7eb765574e29b5d9c2c"),
hexToModNScalar("d47563f52aac6b04b55de236b7c515eb9311757db01e02cff079c3ca6efb063f"),
)
sig := Signature{
R: fromHex("fef45d2892953aa5bbcdb057b5e98b208f1617a7498af7eb765574e29b5d9c2c"),
S: fromHex("d47563f52aac6b04b55de236b7c515eb9311757db01e02cff079c3ca6efb063f"),
}

if !sig.Verify(msgHash.Bytes(), pubKey) {
if !sig.Verify(msgHash.Bytes(), &pubKey) {
b.Errorf("Signature failed to verify")
return
}
b.StartTimer()

for i := 0; i < b.N; i++ {
sig.Verify(msgHash.Bytes(), pubKey)
sig.Verify(msgHash.Bytes(), &pubKey)
}
}

// BenchmarkFieldNormalize benchmarks how long it takes the internal field
// to perform normalization (which includes modular reduction).
func BenchmarkFieldNormalize(b *testing.B) {
// The normalize function is constant time so default value is fine.
var f FieldVal
f := new(fieldVal)
for i := 0; i < b.N; i++ {
f.Normalize()
}
Expand All @@ -360,7 +138,7 @@ func BenchmarkParseCompressedPubKey(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
pk, err = ParsePubKey(rawPk)
pk, err = ParsePubKey(rawPk, S256())
}
_ = pk
_ = err
Expand Down
Loading

0 comments on commit d604973

Please sign in to comment.