Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

Commit

Permalink
crypto/sr25519: Switch the sr25519 provider to curve25519-voi
Browse files Browse the repository at this point in the history
Switch the sr25519 implementation to curve25519-voi for better
performance, and code quality.

Performance comparisions should still be taken with a grain of salt
for the following reasons:

 * curve25519-voi's sr25519 support can use more optimization.
 * go-schnorrkel cuts corners in places by:
   * Not doing delinearization at all when verifying batches
   * Not using the secret key nonce at all when signing.
   * Not sampling random scalars at all when verifying batches,
     unless the import is bumped.

WARNING: This is a breaking change as the original tendermint sr25519
support expands the MiniSecretKey twice, while this implementation
only does it once.
  • Loading branch information
Yawning committed Jun 25, 2021
1 parent a8bd268 commit 3467207
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 167 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi
and `TxInfo`. (@alexanderbez)

- Blockchain Protocol
- [crypto/sr25519] \#6526 Do not re-execute the Ed25519-style key derivation step when doing signing and verification. The derivation is now done once and only once. This breaks `sr25519.GenPrivKeyFromSecret` output compatibility. (@Yawning)

- Data Storage
- [store/state/evidence/light] \#5771 Use an order-preserving varint key encoding (@cmwaters)
Expand Down Expand Up @@ -96,6 +97,7 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi
- [types] \#6478 Add `block_id` to `newblock` event (@jeebster)
- [crypto/ed25519] \#5632 Adopt zip215 `ed25519` verification. (@marbar3778)
- [crypto/ed25519] \#6526 Use [curve25519-voi](https://github.com/oasisprotocol/curve25519-voi) for `ed25519` signing and verification. (@Yawning)
- [crypto/sr25519] \#6526 Use [curve25519-voi](https://github.com/oasisprotocol/curve25519-voi) for `sr25519` signing and verification. (@Yawning)
- [privval] \#5603 Add `--key` to `init`, `gen_validator`, `testnet` & `unsafe_reset_priv_validator` for use in generating `secp256k1` keys.
- [privval] \#5725 Add gRPC support to private validator.
- [privval] \#5876 `tendermint show-validator` will query the remote signer if gRPC is being used (@marbar3778)
Expand Down
77 changes: 19 additions & 58 deletions crypto/sr25519/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,44 @@ package sr25519
import (
"fmt"

schnorrkel "github.com/ChainSafe/go-schnorrkel"
"github.com/oasisprotocol/curve25519-voi/primitives/sr25519"

"github.com/tendermint/tendermint/crypto"
)

var _ crypto.BatchVerifier = &BatchVerifier{}

// BatchVerifier implements batch verification for sr25519.
// https://github.com/ChainSafe/go-schnorrkel is used for batch verification
type BatchVerifier struct {
*schnorrkel.BatchVerifier

// The go-schnorrkel API is terrible for performance if the chance
// that a batch fails is non-negligible, exact information about
// which signature failed is something that is desired, and the
// system is not resource constrained.
//
// The tendermint case meets all of the criteria, so emulate a
// one-shot with fallback API so that libraries that do provide
// sensible behavior aren't penalized.
pubKeys []crypto.PubKey
messages [][]byte
signatures [][]byte
*sr25519.BatchVerifier
}

func NewBatchVerifier() crypto.BatchVerifier {
return &BatchVerifier{schnorrkel.NewBatchVerifier(), nil, nil, nil}
return &BatchVerifier{sr25519.NewBatchVerifier()}
}

func (b *BatchVerifier) Add(key crypto.PubKey, msg, sig []byte) error {
var sig64 [SignatureSize]byte
copy(sig64[:], sig)
signature := new(schnorrkel.Signature)
err := signature.Decode(sig64)
if err != nil {
return fmt.Errorf("unable to decode signature: %w", err)
}

signingContext := schnorrkel.NewSigningContext([]byte{}, msg)

var pk [PubKeySize]byte
copy(pk[:], key.Bytes())

err = b.BatchVerifier.Add(signingContext, signature, schnorrkel.NewPublicKey(pk))
if err == nil {
b.pubKeys = append(b.pubKeys, key)
b.messages = append(b.messages, msg)
b.signatures = append(b.signatures, sig)
func (b *BatchVerifier) Add(key crypto.PubKey, msg, signature []byte) error {
pk, ok := key.(PubKey)
if !ok {
return fmt.Errorf("sr25519: pubkey is not sr25519")
}

return err
}

func (b *BatchVerifier) Verify() (bool, []bool) {
// Explicitly mimic ed25519consensus/curve25519-voi behavior.
if len(b.pubKeys) == 0 {
return false, nil
var srpk sr25519.PublicKey
if err := srpk.UnmarshalBinary(pk); err != nil {
return fmt.Errorf("sr25519: invalid public key: %w", err)
}

// Optimistically assume everything will succeed.
valid := make([]bool, len(b.pubKeys))
for i := range valid {
valid[i] = true
var sig sr25519.Signature
if err := sig.UnmarshalBinary(signature); err != nil {
return fmt.Errorf("sr25519: unable to decode signature: %w", err)
}

// Fast path, every signature may be valid.
if b.BatchVerifier.Verify() {
return true, valid
}
st := signingCtx.NewTranscriptBytes(msg)
b.BatchVerifier.Add(&srpk, st, &sig)

// Fall-back to serial verification.
allValid := true
for i := range b.pubKeys {
pk, msg, sig := b.pubKeys[i], b.messages[i], b.signatures[i]
valid[i] = pk.VerifySignature(msg, sig)
allValid = allValid && valid[i]
}
return nil
}

return allValid, valid
func (b *BatchVerifier) Verify() (bool, []bool) {
return b.BatchVerifier.Verify(crypto.CReader())
}
37 changes: 26 additions & 11 deletions crypto/sr25519/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,36 @@ func BenchmarkVerification(b *testing.B) {
}

func BenchmarkVerifyBatch(b *testing.B) {
for _, n := range []int{1, 8, 64, 1024} {
n := n
b.Run(fmt.Sprintf("sig-count-%d", n), func(b *testing.B) {
b.ReportAllocs()
v := NewBatchVerifier()
for i := 0; i < n; i++ {
msg := []byte("BatchVerifyTest")

for _, sigsCount := range []int{1, 8, 64, 1024} {
sigsCount := sigsCount
b.Run(fmt.Sprintf("sig-count-%d", sigsCount), func(b *testing.B) {
// Pre-generate all of the keys, and signatures, but do not
// benchmark key-generation and signing.
pubs := make([]crypto.PubKey, 0, sigsCount)
sigs := make([][]byte, 0, sigsCount)
for i := 0; i < sigsCount; i++ {
priv := GenPrivKey()
pub := priv.PubKey()
msg := []byte("BatchVerifyTest")
sig, _ := priv.Sign(msg)
err := v.Add(pub, msg, sig)
require.NoError(b, err)
pubs = append(pubs, priv.PubKey().(PubKey))
sigs = append(sigs, sig)
}
b.ResetTimer()

b.ReportAllocs()
// NOTE: dividing by n so that metrics are per-signature
for i := 0; i < b.N/n; i++ {
for i := 0; i < b.N/sigsCount; i++ {
// The benchmark could just benchmark the Verify()
// routine, but there is non-trivial overhead associated
// with BatchVerifier.Add(), which should be included
// in the benchmark.
v := NewBatchVerifier()
for i := 0; i < sigsCount; i++ {
err := v.Add(pubs[i], msg, sigs[i])
require.NoError(b, err)
}

if ok, _ := v.Verify(); !ok {
b.Fatal("signature set failed batch verification")
}
Expand Down
12 changes: 1 addition & 11 deletions crypto/sr25519/encoding.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
package sr25519

import (
"github.com/tendermint/tendermint/crypto"
tmjson "github.com/tendermint/tendermint/libs/json"
)

var _ crypto.PrivKey = PrivKey{}
import tmjson "github.com/tendermint/tendermint/libs/json"

const (
PrivKeyName = "tendermint/PrivKeySr25519"
PubKeyName = "tendermint/PubKeySr25519"

// SignatureSize is the size of an Edwards25519 signature. Namely the size of a compressed
// Sr25519 point, and a field element. Both of which are 32 bytes.
SignatureSize = 64
)

func init() {

tmjson.RegisterType(PubKey{}, PubKeyName)
tmjson.RegisterType(PrivKey{}, PrivKeyName)
}
Loading

0 comments on commit 3467207

Please sign in to comment.