From 6ffdf181f2c1aec45739bf56116880ec18bca264 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 15 Mar 2021 10:58:49 +0000 Subject: [PATCH] crypto: ed25519 & sr25519 batch verification (#6120) Co-authored-by: Aleksandr Bezobchuk Co-authored-by: Anton Kaliaev --- CHANGELOG_PENDING.md | 3 + crypto/batch/batch.go | 22 +++ crypto/crypto.go | 10 ++ crypto/ed25519/bench_test.go | 26 +++ crypto/ed25519/ed25519.go | 32 ++++ crypto/ed25519/ed25519_test.go | 24 +++ crypto/sr25519/batch.go | 42 +++++ crypto/sr25519/bench_test.go | 26 +++ crypto/sr25519/privkey.go | 2 +- crypto/sr25519/pubkey.go | 4 +- crypto/sr25519/sr25519_test.go | 25 +++ go.mod | 4 +- go.sum | 6 + types/validator_set.go | 317 ++++++++++++++++++++++++++------- types/validator_set_test.go | 72 ++++++++ 15 files changed, 550 insertions(+), 65 deletions(-) create mode 100644 crypto/batch/batch.go create mode 100644 crypto/sr25519/batch.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 3d7fe5d6e..a773c2db9 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -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 diff --git a/crypto/batch/batch.go b/crypto/batch/batch.go new file mode 100644 index 000000000..e53ceb319 --- /dev/null +++ b/crypto/batch/batch.go @@ -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 +} diff --git a/crypto/crypto.go b/crypto/crypto.go index 9a341f9ac..5d68b578b 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -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 +} diff --git a/crypto/ed25519/bench_test.go b/crypto/ed25519/bench_test.go index 47897cde6..ca48aa294 100644 --- a/crypto/ed25519/bench_test.go +++ b/crypto/ed25519/bench_test.go @@ -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") + } + } + }) + } +} diff --git a/crypto/ed25519/ed25519.go b/crypto/ed25519/ed25519.go index 30d470485..5b936618c 100644 --- a/crypto/ed25519/ed25519.go +++ b/crypto/ed25519/ed25519.go @@ -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() +} diff --git a/crypto/ed25519/ed25519_test.go b/crypto/ed25519/ed25519_test.go index 8c48847c0..6a139e9e2 100644 --- a/crypto/ed25519/ed25519_test.go +++ b/crypto/ed25519/ed25519_test.go @@ -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()) +} diff --git a/crypto/sr25519/batch.go b/crypto/sr25519/batch.go new file mode 100644 index 000000000..9f8665668 --- /dev/null +++ b/crypto/sr25519/batch.go @@ -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() +} diff --git a/crypto/sr25519/bench_test.go b/crypto/sr25519/bench_test.go index 0561eff72..6c3b4e21d 100644 --- a/crypto/sr25519/bench_test.go +++ b/crypto/sr25519/bench_test.go @@ -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") + } + } + }) + } +} diff --git a/crypto/sr25519/privkey.go b/crypto/sr25519/privkey.go index e77ca375c..f85c7af56 100644 --- a/crypto/sr25519/privkey.go +++ b/crypto/sr25519/privkey.go @@ -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. diff --git a/crypto/sr25519/pubkey.go b/crypto/sr25519/pubkey.go index 87805cacb..8983a6d65 100644 --- a/crypto/sr25519/pubkey.go +++ b/crypto/sr25519/pubkey.go @@ -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 } diff --git a/crypto/sr25519/sr25519_test.go b/crypto/sr25519/sr25519_test.go index 1efe31cad..60c7a2999 100644 --- a/crypto/sr25519/sr25519_test.go +++ b/crypto/sr25519/sr25519_test.go @@ -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") + } +} diff --git a/go.mod b/go.mod index f60939352..5ad924eae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f396352b5..688a64514 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/types/validator_set.go b/types/validator_set.go index 3c797db76..9b3647605 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -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 +} diff --git a/types/validator_set_test.go b/types/validator_set_test.go index 84fdcdf48..546df11fa 100644 --- a/types/validator_set_test.go +++ b/types/validator_set_test.go @@ -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) + } + }) + } +}