mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-09 22:47:24 +00:00
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>
This commit is contained in:
@@ -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
crypto/batch/batch.go
Normal file
22
crypto/batch/batch.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
crypto/sr25519/batch.go
Normal file
42
crypto/sr25519/batch.go
Normal 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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
4
go.mod
4
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
|
||||
|
||||
6
go.sum
6
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=
|
||||
|
||||
@@ -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]
|
||||
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
|
||||
}
|
||||
|
||||
talliedVotingPower += val.VotingPower
|
||||
|
||||
// return as soon as +2/3 of the signatures are verified
|
||||
if talliedVotingPower > votingPowerNeeded {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
|
||||
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
talliedVotingPower += val.VotingPower
|
||||
|
||||
// return as soon as +2/3 of the signatures are verified
|
||||
if talliedVotingPower > votingPowerNeeded {
|
||||
} 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
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
|
||||
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user