Skip to content

Commit

Permalink
Improve performance by avoiding scalar mult during search phase of key.
Browse files Browse the repository at this point in the history
  • Loading branch information
innix committed Jan 6, 2022
1 parent 912eebf commit 703e6d8
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 62 deletions.
111 changes: 111 additions & 0 deletions internal/ed25519/ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package ed25519

import (
"bytes"
cryptorand "crypto/rand"
"crypto/sha512"
"errors"
"fmt"
"io"

"github.com/oasisprotocol/curve25519-voi/curve"
"github.com/oasisprotocol/curve25519-voi/curve/scalar"
)

const (
// PublicKeySize is the size, in bytes, of public keys as used in this package.
PublicKeySize = 32

// PrivateKeySize is the size, in bytes, of private keys as used in this package.
PrivateKeySize = 64

// SeedSize is the size, in bytes, of private key seeds.
SeedSize = 32
)

// PrivateKey is the type of Ed25519 private keys.
type PrivateKey []byte

// PublicKey is the type of Ed25519 public keys.
type PublicKey []byte

// KeyPair is a type with both Ed25519 keys.
type KeyPair struct {
// PublicKey is the public key of the Ed25519 key pair.
PublicKey PublicKey

// PrivateKey is the private key of the Ed25519 key pair.
PrivateKey PrivateKey
}

// Validate performs sanity checks to ensure that the public and private keys match.
func (kp *KeyPair) Validate() error {
pk, err := getPublicKeyFromPrivateKey(kp.PrivateKey)
if err != nil {
return fmt.Errorf("could not compute public key from private key: %w", err)
}

if !bytes.Equal(kp.PublicKey, pk) {
return errors.New("keys do not match")
}

return nil
}

func GenerateKey(rand io.Reader) (*KeyPair, error) {
if rand == nil {
rand = cryptorand.Reader
}

seed := make([]byte, SeedSize)
if _, err := io.ReadFull(rand, seed); err != nil {
return nil, err
}

sk := make([]byte, PrivateKeySize)
newKeyFromSeed(sk, seed)

// Private key does not contain the public key in this implementation, so we
// need to compute it instead.
pk, err := getPublicKeyFromPrivateKey(sk)
if err != nil {
return nil, err
}

return &KeyPair{
PublicKey: pk,
PrivateKey: sk,
}, nil
}

func newKeyFromSeed(sk, seed []byte) {
if l := len(seed); l != SeedSize {
panic(fmt.Sprintf("bad seed length: %d", l))
}

digest := sha512.Sum512(seed)
clampSecretKey(&digest)
copy(sk, digest[:])
}

func getPublicKeyFromPrivateKey(sk []byte) ([]byte, error) {
if l := len(sk); l != PrivateKeySize {
panic(fmt.Errorf("bad private key length: %d", len(sk)))
}

sc, err := scalar.NewFromBits(sk[:scalar.ScalarSize])
if err != nil {
return nil, err
}

pk := curve.NewCompressedEdwardsY()
pk.SetEdwardsPoint(curve.NewEdwardsPoint().MulBasepoint(curve.ED25519_BASEPOINT_TABLE, sc))

return pk[:], nil
}

func clampSecretKey(sk *[64]byte) {
sk[0] &= 248
sk[31] &= 63
sk[31] |= 64
}
16 changes: 16 additions & 0 deletions internal/ed25519/ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ed25519_test

import (
"testing"

"github.com/innix/shrek/internal/ed25519"
)

func BenchmarkGenerateNewKey(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := ed25519.GenerateKey(nil)
if err != nil {
b.Fatal(err)
}
}
}
114 changes: 114 additions & 0 deletions internal/ed25519/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package ed25519

import (
"errors"
"fmt"
"io"
"math"

"github.com/oasisprotocol/curve25519-voi/curve"
"github.com/oasisprotocol/curve25519-voi/curve/scalar"
)

type keyIterator struct {
kp *KeyPair
eightPt *curve.EdwardsPoint

pt *curve.EdwardsPoint
sc *scalar.Scalar

counter uint64
}

// NewKeyIterator creates and initializes a new Ed25519 key iterator.
// The iterator is NOT thread safe; you must create a separate iterator for
// each worker instead of sharing a single instance.
func NewKeyIterator(rand io.Reader) (*keyIterator, error) {
eightPt := curve.NewEdwardsPoint()
eightPt = eightPt.MulBasepoint(curve.ED25519_BASEPOINT_TABLE, scalar.NewFromUint64(8))

it := &keyIterator{
eightPt: eightPt,
}
if _, err := it.init(rand); err != nil {
return nil, err
}

return it, nil
}

func (it *keyIterator) Next() bool {
const maxCounter = math.MaxUint64 - 8

if it.counter > uint64(maxCounter) {
return false
}

it.pt = it.pt.Add(it.pt, it.eightPt)
it.counter += 8

return true
}

func (it *keyIterator) PublicKey() PublicKey {
var pk curve.CompressedEdwardsY
pk.SetEdwardsPoint(it.pt)

return pk[:]
}

func (it *keyIterator) PrivateKey() (PrivateKey, error) {
sc := scalar.New().Set(it.sc)

if it.counter > 0 {
if err := scalarAdd(sc, it.counter); err != nil {
return nil, err
}
}

sk := make([]byte, PrivateKeySize)
if err := sc.ToBytes(sk[:scalar.ScalarSize]); err != nil {
return nil, fmt.Errorf("could not pack scalar into byte array: %w", err)
}
copy(sk[scalar.ScalarSize:], it.kp.PrivateKey[scalar.ScalarSize:])

// Sanity check.
if !((sk[0] & 248) == sk[0]) || !(((sk[31] & 63) | 64) == sk[31]) {
return nil, errors.New("sanity check on private key failed")
}

return sk, nil
}

func (it *keyIterator) init(rand io.Reader) (*KeyPair, error) {
kp, err := GenerateKey(rand)
if err != nil {
return nil, err
}

// Parse private key.
sk, err := scalar.NewFromBits(kp.PrivateKey[:scalar.ScalarSize])
if err != nil {
return nil, fmt.Errorf("could not parse scalar from private key: %w", err)
}

// Parse public key.
cpt, err := curve.NewCompressedEdwardsYFromBytes(kp.PublicKey)
if err != nil {
return nil, fmt.Errorf("could not parse point from public key: %w", err)
}
pk := curve.NewEdwardsPoint()
if _, err := pk.SetCompressedY(cpt); err != nil {
return nil, fmt.Errorf("could not decompress point from public key: %w", err)
}

// Cache data so it can be used later.
it.kp = kp
it.sc = sk
it.pt = pk

// Reset counter.
it.counter = 0

return kp, nil
}
22 changes: 22 additions & 0 deletions internal/ed25519/iterator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ed25519_test

import (
"testing"

"github.com/innix/shrek/internal/ed25519"
)

func BenchmarkKeyIterator_PublicKeyAndNext(b *testing.B) {
it, err := ed25519.NewKeyIterator(nil)
if err != nil {
b.Fatalf("Could not create key iterator: %v", err)
}

for i := 0; i < b.N; i++ {
_ = it.PublicKey()
if err != nil {
b.Fatal(err)
}
it.Next()
}
}
33 changes: 33 additions & 0 deletions internal/ed25519/scalaradd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ed25519

import (
"github.com/oasisprotocol/curve25519-voi/curve/scalar"
)

func scalarAdd(dst *scalar.Scalar, v uint64) error {
var dstb [32]byte

if err := dst.ToBytes(dstb[:]); err != nil {
return err
}

scalarAddBytes(&dstb, v)

if _, err := dst.SetBits(dstb[:]); err != nil {
return err
}

return nil
}

func scalarAddBytes(dst *[32]byte, v uint64) {
var carry uint32

for i := 0; i < 32; i++ {
carry += uint32(dst[i]) + uint32(v&0xFF)
dst[i] = byte(carry & 0xFF)
carry >>= 8

v >>= 8
}
}
36 changes: 32 additions & 4 deletions miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@ package shrek

import (
"context"
"errors"
"fmt"
"io"

"github.com/innix/shrek/internal/ed25519"
)

func MineOnionHostName(ctx context.Context, rand io.Reader, m Matcher) (*OnionAddress, error) {
hostname := make([]byte, EncodedPublicKeySize)

for ctx.Err() == nil {
addr, err := GenerateOnionAddress(rand)
if err != nil {
return nil, fmt.Errorf("could not generate key pair: %w", err)
it, err := ed25519.NewKeyIterator(rand)
if err != nil {
return nil, fmt.Errorf("could not create key iterator: %w", err)
}

for more := true; ctx.Err() == nil; more = it.Next() {
if !more {
return nil, errors.New("searched entire address space and no match was found")
}

addr := &OnionAddress{
PublicKey: it.PublicKey(),

// The private key is not needed to generate the hostname. So to avoid pointless
// computation, we wait until a match has been found first.
SecretKey: nil,
}

// The approximate encoder only generates the first 51 bytes of the hostname accurately;
Expand All @@ -34,6 +49,19 @@ func MineOnionHostName(ctx context.Context, rand io.Reader, m Matcher) (*OnionAd
continue
}

// Compute private key after a match has been found.
sk, err := it.PrivateKey()
if err != nil {
return nil, fmt.Errorf("could not compute private key: %w", err)
}
addr.SecretKey = sk

// Sanity check keys retrieved from iterator.
kp := &ed25519.KeyPair{PublicKey: addr.PublicKey, PrivateKey: addr.SecretKey}
if err := kp.Validate(); err != nil {
return nil, fmt.Errorf("key validation failed: %w", err)
}

return addr, nil
}

Expand Down
Loading

0 comments on commit 703e6d8

Please sign in to comment.