Browse Source

crypto: ed25519 & sr25519 batch verification (#6120)

Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
Co-authored-by: Anton Kaliaev <anton.kalyaev@gmail.com>
pull/6247/head
Marko 3 years ago
committed by GitHub
parent
commit
6ffdf181f2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 550 additions and 65 deletions
  1. +3
    -0
      CHANGELOG_PENDING.md
  2. +22
    -0
      crypto/batch/batch.go
  3. +10
    -0
      crypto/crypto.go
  4. +26
    -0
      crypto/ed25519/bench_test.go
  5. +32
    -0
      crypto/ed25519/ed25519.go
  6. +24
    -0
      crypto/ed25519/ed25519_test.go
  7. +42
    -0
      crypto/sr25519/batch.go
  8. +26
    -0
      crypto/sr25519/bench_test.go
  9. +1
    -1
      crypto/sr25519/privkey.go
  10. +2
    -2
      crypto/sr25519/pubkey.go
  11. +25
    -0
      crypto/sr25519/sr25519_test.go
  12. +2
    -2
      go.mod
  13. +6
    -0
      go.sum
  14. +257
    -60
      types/validator_set.go
  15. +72
    -0
      types/validator_set_test.go

+ 3
- 0
CHANGELOG_PENDING.md View File

@ -72,6 +72,9 @@ Friendly reminder: We have a [bug bounty program](https://hackerone.com/tendermi
- [rpc/client/http] \#6163 Do not drop events even if the `out` channel is full (@melekes)
- [node] \#6059 Validate and complete genesis doc before saving to state store (@silasdavis)
- [state] \#6067 Batch save state data (@githubsands & @cmwaters)
- [crypto] \#6120 Implement batch verification interface for ed25519 and sr25519. (@marbar3778)
- [types] \#6120 use batch verification for verifying commits signatures.
- If the key type supports the batch verification API it will try to batch verify. If the verification fails we will single verify each signature.
- [privval/file] \#6185 Return error on `LoadFilePV`, `LoadFilePVEmptyState`. Allows for better programmatic control of Tendermint.
### BUG FIXES


+ 22
- 0
crypto/batch/batch.go View File

@ -0,0 +1,22 @@
package batch
import (
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/sr25519"
)
// CreateBatchVerifier checks if a key type implements the batch verifier interface.
// Currently only ed25519 & sr25519 supports batch verification.
func CreateBatchVerifier(pk crypto.PubKey) (crypto.BatchVerifier, bool) {
switch pk.Type() {
case ed25519.KeyType:
return ed25519.NewBatchVerifier(), true
case sr25519.KeyType:
return sr25519.NewBatchVerifier(), true
}
// case where the key does not support batch verification
return nil, false
}

+ 10
- 0
crypto/crypto.go View File

@ -40,3 +40,13 @@ type Symmetric interface {
Encrypt(plaintext []byte, secret []byte) (ciphertext []byte)
Decrypt(ciphertext []byte, secret []byte) (plaintext []byte, err error)
}
// If a new key type implements batch verification,
// the key type must be registered in github.com/tendermint/tendermint/crypto/batch
type BatchVerifier interface {
// Add appends an entry into the BatchVerifier.
Add(key PubKey, message, signature []byte) error
// Verify verifies all the entries in the BatchVerifier.
// If the verification fails it is unknown which entry failed and each entry will need to be verified individually.
Verify() bool
}

+ 26
- 0
crypto/ed25519/bench_test.go View File

@ -1,9 +1,11 @@
package ed25519
import (
"fmt"
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/internal/benchmarking"
)
@ -24,3 +26,27 @@ func BenchmarkVerification(b *testing.B) {
priv := GenPrivKey()
benchmarking.BenchmarkVerification(b, priv)
}
func BenchmarkVerifyBatch(b *testing.B) {
for _, sigsCount := range []int{1, 8, 64, 1024} {
sigsCount := sigsCount
b.Run(fmt.Sprintf("sig-count-%d", sigsCount), func(b *testing.B) {
b.ReportAllocs()
v := NewBatchVerifier()
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)
}
// NOTE: dividing by n so that metrics are per-signature
for i := 0; i < b.N/sigsCount; i++ {
if !v.Verify() {
b.Fatal("signature set failed batch verification")
}
}
})
}
}

+ 32
- 0
crypto/ed25519/ed25519.go View File

@ -4,6 +4,7 @@ import (
"bytes"
"crypto/ed25519"
"crypto/subtle"
"errors"
"fmt"
"io"
@ -170,3 +171,34 @@ func (pubKey PubKey) Equals(other crypto.PubKey) bool {
return false
}
var _ crypto.BatchVerifier = &BatchVerifier{}
// BatchVerifier implements batch verification for ed25519.
// https://github.com/hdevalence/ed25519consensus is used for batch verification
type BatchVerifier struct {
ed25519consensus.BatchVerifier
}
func NewBatchVerifier() crypto.BatchVerifier {
return &BatchVerifier{ed25519consensus.NewBatchVerifier()}
}
func (b *BatchVerifier) Add(key crypto.PubKey, msg, signature []byte) error {
if l := len(key.Bytes()); l != PubKeySize {
return fmt.Errorf("pubkey size is incorrect; expected: %d, got %d", PubKeySize, l)
}
// check that the signature is the correct length & the last byte is set correctly
if len(signature) != SignatureSize || signature[63]&224 != 0 {
return errors.New("invalid signature")
}
b.BatchVerifier.Add(ed25519.PublicKey(key.Bytes()), msg, signature)
return nil
}
func (b *BatchVerifier) Verify() bool {
return b.BatchVerifier.Verify()
}

+ 24
- 0
crypto/ed25519/ed25519_test.go View File

@ -28,3 +28,27 @@ func TestSignAndValidateEd25519(t *testing.T) {
assert.False(t, pubKey.VerifySignature(msg, sig))
}
func TestBatchSafe(t *testing.T) {
v := ed25519.NewBatchVerifier()
for i := 0; i <= 38; i++ {
priv := ed25519.GenPrivKey()
pub := priv.PubKey()
var msg []byte
if i%2 == 0 {
msg = []byte("easter")
} else {
msg = []byte("egg")
}
sig, err := priv.Sign(msg)
require.NoError(t, err)
err = v.Add(pub, msg, sig)
require.NoError(t, err)
}
require.True(t, v.Verify())
}

+ 42
- 0
crypto/sr25519/batch.go View File

@ -0,0 +1,42 @@
package sr25519
import (
"fmt"
schnorrkel "github.com/ChainSafe/go-schnorrkel"
"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
}
func NewBatchVerifier() crypto.BatchVerifier {
return BatchVerifier{schnorrkel.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())
return b.BatchVerifier.Add(signingContext, signature, schnorrkel.NewPublicKey(pk))
}
func (b BatchVerifier) Verify() bool {
return b.BatchVerifier.Verify()
}

+ 26
- 0
crypto/sr25519/bench_test.go View File

@ -1,9 +1,11 @@
package sr25519
import (
"fmt"
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/internal/benchmarking"
)
@ -24,3 +26,27 @@ func BenchmarkVerification(b *testing.B) {
priv := GenPrivKey()
benchmarking.BenchmarkVerification(b, priv)
}
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++ {
priv := GenPrivKey()
pub := priv.PubKey()
msg := []byte("BatchVerifyTest")
sig, _ := priv.Sign(msg)
err := v.Add(pub, msg, sig)
require.NoError(b, err)
}
// NOTE: dividing by n so that metrics are per-signature
for i := 0; i < b.N/n; i++ {
if !v.Verify() {
b.Fatal("signature set failed batch verification")
}
}
})
}
}

+ 1
- 1
crypto/sr25519/privkey.go View File

@ -70,7 +70,7 @@ func (privKey PrivKey) Equals(other crypto.PrivKey) bool {
}
func (privKey PrivKey) Type() string {
return keyType
return KeyType
}
// GenPrivKey generates a new sr25519 private key.


+ 2
- 2
crypto/sr25519/pubkey.go View File

@ -15,7 +15,7 @@ var _ crypto.PubKey = PubKey{}
// PubKeySize is the number of bytes in an Sr25519 public key.
const (
PubKeySize = 32
keyType = "sr25519"
KeyType = "sr25519"
)
// PubKeySr25519 implements crypto.PubKey for the Sr25519 signature scheme.
@ -72,6 +72,6 @@ func (pubKey PubKey) Equals(other crypto.PubKey) bool {
}
func (pubKey PubKey) Type() string {
return keyType
return KeyType
}

+ 25
- 0
crypto/sr25519/sr25519_test.go View File

@ -29,3 +29,28 @@ func TestSignAndValidateSr25519(t *testing.T) {
assert.False(t, pubKey.VerifySignature(msg, sig))
}
func TestBatchSafe(t *testing.T) {
v := sr25519.NewBatchVerifier()
for i := 0; i <= 38; i++ {
priv := sr25519.GenPrivKey()
pub := priv.PubKey()
var msg []byte
if i%2 == 0 {
msg = []byte("easter")
} else {
msg = []byte("egg")
}
sig, err := priv.Sign(msg)
require.NoError(t, err)
err = v.Add(pub, msg, sig)
require.NoError(t, err)
}
if !v.Verify() {
t.Error("failed batch verification")
}
}

+ 2
- 2
go.mod View File

@ -4,7 +4,7 @@ go 1.15
require (
github.com/BurntSushi/toml v0.3.1
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d
github.com/ChainSafe/go-schnorrkel v0.0.0-20210222182958-bd440c890782
github.com/Workiva/go-datastructures v1.0.52
github.com/btcsuite/btcd v0.21.0-beta
github.com/btcsuite/btcutil v1.0.2
@ -20,7 +20,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/gtank/merlin v0.1.1
github.com/hdevalence/ed25519consensus v0.0.0-20201207055737-7fde80a9d5ff
github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87
github.com/libp2p/go-buffer-pool v0.0.2
github.com/minio/highwayhash v1.0.1
github.com/pkg/errors v0.9.1


+ 6
- 0
go.sum View File

@ -13,11 +13,15 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0-alpha.2 h1:EWbZLqGEPSIj2W69gx04KtNVkyPIfe3uj0DhDQJonbQ=
filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
filippo.io/edwards25519 v1.0.0-beta.2 h1:/BZRNzm8N4K4eWfK28dL4yescorxtO7YG1yun8fy+pI=
filippo.io/edwards25519 v1.0.0-beta.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg=
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4=
github.com/ChainSafe/go-schnorrkel v0.0.0-20210222182958-bd440c890782 h1:lwmjzta2Xu+3rPVY/VeNQj2xfNkyih4CwyRxYg3cpRQ=
github.com/ChainSafe/go-schnorrkel v0.0.0-20210222182958-bd440c890782/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -290,6 +294,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hdevalence/ed25519consensus v0.0.0-20201207055737-7fde80a9d5ff h1:LeVKjw8pcDQj7WVVnbFvbD7ovcv+r/l15ka1NH6Lswc=
github.com/hdevalence/ed25519consensus v0.0.0-20201207055737-7fde80a9d5ff/go.mod h1:Feit0l8NcNO4g69XNjwvsR0LGcwMMfzI1TF253rOIlQ=
github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87 h1:uUjLpLt6bVvZ72SQc/B4dXcPBw4Vgd7soowdRl52qEM=
github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87/go.mod h1:XGsKKeXxeRr95aEOgipvluMPlgjr7dGlk9ZTWOjcUcg=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=


+ 257
- 60
types/validator_set.go View File

@ -9,6 +9,7 @@ import (
"sort"
"strings"
"github.com/tendermint/tendermint/crypto/batch"
"github.com/tendermint/tendermint/crypto/merkle"
tmmath "github.com/tendermint/tendermint/libs/math"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
@ -678,30 +679,49 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
blockID, commit.BlockID)
}
talliedVotingPower := int64(0)
votingPowerNeeded := vals.TotalVotingPower() * 2 / 3
for idx, commitSig := range commit.Signatures {
if commitSig.Absent() {
continue // OK, some signatures can be absent.
}
var (
talliedVotingPower int64 = 0
err error
cacheSignBytes = make(map[string][]byte, len(commit.Signatures))
)
// The vals and commit have a 1-to-1 correspondance.
// This means we don't need the validator address or to do any lookup.
val := vals.Validators[idx]
bv, ok := batch.CreateBatchVerifier(vals.GetProposer().PubKey)
if ok && len(commit.Signatures) > 1 {
for idx, commitSig := range commit.Signatures {
if commitSig.Absent() {
continue // OK, some signatures can be absent.
}
// Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
// The vals and commit have a 1-to-1 correspondance.
// This means we don't need the validator address or to do any lookup.
val := vals.Validators[idx]
// Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
// cache the signBytes in case batch verification fails
cacheSignBytes[string(val.PubKey.Bytes())] = voteSignBytes
// add the key, sig and message to the verifier
if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
return err
}
// Good!
if commitSig.ForBlock() {
talliedVotingPower += val.VotingPower
}
}
// Good!
if commitSig.ForBlock() {
talliedVotingPower += val.VotingPower
if !bv.Verify() {
talliedVotingPower, err = verifyCommitSingle(chainID, vals, commit, cacheSignBytes)
if err != nil {
return err
}
}
} else {
talliedVotingPower, err = verifyCommitSingle(chainID, vals, commit, cacheSignBytes)
if err != nil {
return err
}
// else {
// It's OK. We include stray signatures (~votes for nil) to measure
// validator availability.
// }
}
if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed {
@ -738,30 +758,58 @@ func (vals *ValidatorSet) VerifyCommitLight(chainID string, blockID BlockID,
talliedVotingPower := int64(0)
votingPowerNeeded := vals.TotalVotingPower() * 2 / 3
for idx, commitSig := range commit.Signatures {
// No need to verify absent or nil votes.
if !commitSig.ForBlock() {
continue
}
cacheSignBytes := make(map[string][]byte, len(commit.Signatures))
var err error
// need to check if batch verification is supported
// if batch is supported and the there are more than x key(s) run batch, otherwise run single.
// if batch verification fails reset tally votes to 0 and single verify until we have 2/3+
// check if the key supports batch verification
bv, ok := batch.CreateBatchVerifier(vals.GetProposer().PubKey)
if ok && len(commit.Signatures) > 1 {
for idx, commitSig := range commit.Signatures {
// No need to verify absent or nil votes.
if !commitSig.ForBlock() {
continue
}
// The vals and commit have a 1-to-1 correspondance.
// This means we don't need the validator address or to do any lookup.
val := vals.Validators[idx]
// The vals and commit have a 1-to-1 correspondance.
// This means we don't need the validator address or to do any lookup.
val := vals.Validators[idx]
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
cacheSignBytes[string(val.PubKey.Bytes())] = voteSignBytes
// add the key, sig and message to the verifier
if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
return err
}
// Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
}
talliedVotingPower += val.VotingPower
talliedVotingPower += val.VotingPower
// return as soon as +2/3 of the signatures are verified
if talliedVotingPower > votingPowerNeeded {
return nil
}
}
// return as soon as +2/3 of the signatures are verified
if talliedVotingPower > votingPowerNeeded {
if !bv.Verify() {
// reset talliedVotingPower to verify enough signatures to meet the 2/3+ threshold
talliedVotingPower, err = verifyCommitLightSingle(
chainID, vals, commit, votingPowerNeeded, cacheSignBytes)
if err != nil {
return err
} else if talliedVotingPower > votingPowerNeeded {
return nil
}
}
} else {
talliedVotingPower, err = verifyCommitLightSingle(
chainID, vals, commit, votingPowerNeeded, cacheSignBytes)
if err != nil {
return err
} else if talliedVotingPower > votingPowerNeeded {
return nil
}
}
return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded}
}
@ -785,6 +833,8 @@ func (vals *ValidatorSet) VerifyCommitLightTrusting(chainID string, commit *Comm
var (
talliedVotingPower int64
seenVals = make(map[int32]int, len(commit.Signatures)) // validator index -> commit index
err error
cacheSignBytes = make(map[string][]byte, len(commit.Signatures))
)
// Safely calculate voting power needed.
@ -794,36 +844,59 @@ func (vals *ValidatorSet) VerifyCommitLightTrusting(chainID string, commit *Comm
}
votingPowerNeeded := totalVotingPowerMulByNumerator / int64(trustLevel.Denominator)
for idx, commitSig := range commit.Signatures {
// No need to verify absent or nil votes.
if !commitSig.ForBlock() {
continue
}
// We don't know the validators that committed this block, so we have to
// check for each vote if its validator is already known.
valIdx, val := vals.GetByAddress(commitSig.ValidatorAddress)
if val != nil {
// check for double vote of validator on the same commit
if firstIndex, ok := seenVals[valIdx]; ok {
secondIndex := idx
return fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
bv, ok := batch.CreateBatchVerifier(vals.GetProposer().PubKey)
if ok && len(commit.Signatures) > 1 {
for idx, commitSig := range commit.Signatures {
// No need to verify absent or nil votes.
if !commitSig.ForBlock() {
continue
}
seenVals[valIdx] = idx
// Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
// We don't know the validators that committed this block, so we have to
// check for each vote if its validator is already known.
valIdx, val := vals.GetByAddress(commitSig.ValidatorAddress)
if val != nil {
// check for double vote of validator on the same commit
if firstIndex, ok := seenVals[valIdx]; ok {
secondIndex := idx
return fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
}
seenVals[valIdx] = idx
// Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
// cache the signed bytes in case we fail verification
cacheSignBytes[string(val.PubKey.Bytes())] = voteSignBytes
// if batch verification is supported add the key, sig and message to the verifier
if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
return err
}
talliedVotingPower += val.VotingPower
if talliedVotingPower > votingPowerNeeded {
return nil
}
}
talliedVotingPower += val.VotingPower
if talliedVotingPower > votingPowerNeeded {
}
if !bv.Verify() {
talliedVotingPower, err = verifyCommitLightTrustingSingle(
chainID, vals, commit, votingPowerNeeded, cacheSignBytes)
if err != nil {
return err
} else if talliedVotingPower > votingPowerNeeded {
return nil
}
}
} else {
talliedVotingPower, err = verifyCommitLightTrustingSingle(
chainID, vals, commit, votingPowerNeeded, cacheSignBytes)
if err != nil {
return err
} else if talliedVotingPower > votingPowerNeeded {
return nil
}
}
return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded}
@ -1101,3 +1174,127 @@ func safeMul(a, b int64) (int64, bool) {
return a * b, false
}
// verifyCommitLightTrustingSingle single verifies commits
// If a key does not support batch verification, or batch verification fails this will be used
// This method is used for light clients, it only checks 2/3+ of the signatures
func verifyCommitLightTrustingSingle(
chainID string, vals *ValidatorSet, commit *Commit, votingPowerNeeded int64,
cachedVals map[string][]byte) (int64, error) {
var (
seenVals = make(map[int32]int, len(commit.Signatures))
talliedVotingPower int64 = 0
)
for idx, commitSig := range commit.Signatures {
// No need to verify absent or nil votes.
if !commitSig.ForBlock() {
continue
}
var voteSignBytes []byte
// We don't know the validators that committed this block, so we have to
// check for each vote if its validator is already known.
valIdx, val := vals.GetByAddress(commitSig.ValidatorAddress)
if val != nil {
// check for double vote of validator on the same commit
if firstIndex, ok := seenVals[valIdx]; ok {
secondIndex := idx
return 0, fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
}
seenVals[valIdx] = idx
// Validate signature.
// voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
if val, ok := cachedVals[string(val.PubKey.Bytes())]; !ok {
voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))
} else {
voteSignBytes = val
}
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return 0, fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
}
talliedVotingPower += val.VotingPower
if talliedVotingPower > votingPowerNeeded {
return talliedVotingPower, nil
}
}
}
return talliedVotingPower, nil
}
// verifyCommitLightSingle single verifies commits.
// If a key does not support batch verification, or batch verification fails this will be used
// This method is used for light client and block sync verification, it will only check 2/3+ signatures
func verifyCommitLightSingle(
chainID string, vals *ValidatorSet, commit *Commit, votingPowerNeeded int64,
cachedVals map[string][]byte) (int64, error) {
var talliedVotingPower int64 = 0
for idx, commitSig := range commit.Signatures {
// No need to verify absent or nil votes.
if !commitSig.ForBlock() {
continue
}
// The vals and commit have a 1-to-1 correspondance.
// This means we don't need the validator address or to do any lookup.
var voteSignBytes []byte
val := vals.Validators[idx]
// Check if we have the validator in the cache
if val, ok := cachedVals[string(val.PubKey.Bytes())]; !ok {
voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))
} else {
voteSignBytes = val
}
// Validate signature.
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return 0, fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
}
talliedVotingPower += val.VotingPower
// return as soon as +2/3 of the signatures are verified
if talliedVotingPower > votingPowerNeeded {
return talliedVotingPower, nil
}
}
return talliedVotingPower, nil
}
// verifyCommitSingle single verifies commits.
// If a key does not support batch verification, or batch verification fails this will be used
// This method is used to check all the signatures included in a commit.
// It is used in consensus for validating a block LastCommit.
func verifyCommitSingle(chainID string, vals *ValidatorSet, commit *Commit,
cachedVals map[string][]byte) (int64, error) {
var talliedVotingPower int64 = 0
for idx, commitSig := range commit.Signatures {
if commitSig.Absent() {
continue // OK, some signatures can be absent.
}
var voteSignBytes []byte
val := vals.Validators[idx]
// Check if we have the validator in the cache
if val, ok := cachedVals[string(val.PubKey.Bytes())]; !ok {
voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))
} else {
voteSignBytes = val
}
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return talliedVotingPower, fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
}
// Good!
if commitSig.ForBlock() {
talliedVotingPower += val.VotingPower
}
}
return talliedVotingPower, nil
}

+ 72
- 0
types/validator_set_test.go View File

@ -1709,3 +1709,75 @@ func BenchmarkUpdates(b *testing.B) {
assert.NoError(b, valSetCopy.UpdateWithChangeSet(newValList))
}
}
func BenchmarkValidatorSet_VerifyCommit_Ed25519(b *testing.B) {
for _, n := range []int{1, 8, 64, 1024} {
n := n
var (
chainID = "test_chain_id"
h = int64(3)
blockID = makeBlockIDRandom()
)
b.Run(fmt.Sprintf("valset size %d", n), func(b *testing.B) {
b.ReportAllocs()
// generate n validators
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, n, int64(n*5))
// create a commit with n validators
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(b, err)
for i := 0; i < b.N/n; i++ {
err = valSet.VerifyCommit(chainID, blockID, h, commit)
assert.NoError(b, err)
}
})
}
}
func BenchmarkValidatorSet_VerifyCommitLight_Ed25519(b *testing.B) {
for _, n := range []int{1, 8, 64, 1024} {
n := n
var (
chainID = "test_chain_id"
h = int64(3)
blockID = makeBlockIDRandom()
)
b.Run(fmt.Sprintf("valset size %d", n), func(b *testing.B) {
b.ReportAllocs()
// generate n validators
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, n, int64(n*5))
// create a commit with n validators
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(b, err)
for i := 0; i < b.N/n; i++ {
err = valSet.VerifyCommitLight(chainID, blockID, h, commit)
assert.NoError(b, err)
}
})
}
}
func BenchmarkValidatorSet_VerifyCommitLightTrusting_Ed25519(b *testing.B) {
for _, n := range []int{1, 8, 64, 1024} {
n := n
var (
chainID = "test_chain_id"
h = int64(3)
blockID = makeBlockIDRandom()
)
b.Run(fmt.Sprintf("valset size %d", n), func(b *testing.B) {
b.ReportAllocs()
// generate n validators
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, n, int64(n*5))
// create a commit with n validators
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(b, err)
for i := 0; i < b.N/n; i++ {
err = valSet.VerifyCommitLightTrusting(chainID, commit, tmmath.Fraction{Numerator: 1, Denominator: 3})
assert.NoError(b, err)
}
})
}
}

Loading…
Cancel
Save