mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-04 04:04:00 +00:00
crypto: Upstream v0.35.x improvements (#9255)
* crypto: Use curve25519-voi This switches the ed25519, sr25519 and merlin provider to curve25519-voi and additionally adopts ZIP-215 semantics for ed25519 verification. * crypto: Implement batch verification interface for ed25519 and sr25519 This commit adds the batch verification interface, but does not enable it for anything. * types: Use batch verification for verifying commits signatures
This commit is contained in:
@@ -58,6 +58,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi
|
||||
- Go API
|
||||
- [all] \#9144 Change spelling from British English to American (@cmwaters)
|
||||
- Rename "Subscription.Cancelled()" to "Subscription.Canceled()" in libs/pubsub
|
||||
- [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)
|
||||
|
||||
- Blockchain Protocol
|
||||
|
||||
@@ -72,6 +73,13 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi
|
||||
- [rpc] \#9276 Added `header` and `header_by_hash` queries to the RPC client (@samricotta)
|
||||
- [abci] \#5706 Added `AbciVersion` to `RequestInfo` allowing applications to check ABCI version when connecting to Tendermint. (@marbar3778)
|
||||
|
||||
- [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)
|
||||
- [crypto] \#6120 Implement batch verification interface for ed25519 and sr25519. (@marbar3778 & @Yawning)
|
||||
- [types] \#6120 use batch verification for verifying commits signatures. (@marbar3778 & @cmwaters & @Yawning)
|
||||
- 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.
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
- [consensus] \#9229 fix round number of `enterPropose` when handling `RoundStepNewRound` timeout. (@fatcat22)
|
||||
|
||||
32
crypto/batch/batch.go
Normal file
32
crypto/batch/batch.go
Normal file
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
|
||||
// SupportsBatchVerifier checks if a key type implements the batch verifier
|
||||
// interface.
|
||||
func SupportsBatchVerifier(pk crypto.PubKey) bool {
|
||||
switch pk.Type() {
|
||||
case ed25519.KeyType, sr25519.KeyType:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -40,3 +40,15 @@ 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, and returns
|
||||
// if every signature in the batch is valid, and a vector of bools
|
||||
// indicating the verification status of each signature (in the order
|
||||
// that signatures were added to the batch).
|
||||
Verify() (bool, []bool)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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 +27,42 @@ func BenchmarkVerification(b *testing.B) {
|
||||
priv := GenPrivKey()
|
||||
benchmarking.BenchmarkVerification(b, priv)
|
||||
}
|
||||
|
||||
func BenchmarkVerifyBatch(b *testing.B) {
|
||||
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()
|
||||
sig, _ := priv.Sign(msg)
|
||||
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/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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package ed25519
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/ed25519"
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/ed25519/extra/cache"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
@@ -15,7 +17,19 @@ import (
|
||||
|
||||
//-------------------------------------
|
||||
|
||||
var _ crypto.PrivKey = PrivKey{}
|
||||
var (
|
||||
_ crypto.PrivKey = PrivKey{}
|
||||
_ crypto.BatchVerifier = &BatchVerifier{}
|
||||
|
||||
// curve25519-voi's Ed25519 implementation supports configurable
|
||||
// verification behavior, and tendermint uses the ZIP-215 verification
|
||||
// semantics.
|
||||
verifyOptions = &ed25519.Options{
|
||||
Verify: ed25519.VerifyOptionsZIP_215,
|
||||
}
|
||||
|
||||
cachingVerifier = cache.NewVerifier(cache.NewLRUCache(cacheSize))
|
||||
)
|
||||
|
||||
const (
|
||||
PrivKeyName = "tendermint/PrivKeyEd25519"
|
||||
@@ -32,6 +46,14 @@ const (
|
||||
SeedSize = 32
|
||||
|
||||
KeyType = "ed25519"
|
||||
|
||||
// cacheSize is the number of public keys that will be cached in
|
||||
// an expanded format for repeated signature verification.
|
||||
//
|
||||
// TODO/perf: Either this should exclude single verification, or be
|
||||
// tuned to `> validatorSize + maxTxnsPerBlock` to avoid cache
|
||||
// thrashing.
|
||||
cacheSize = 4096
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -105,14 +127,12 @@ func GenPrivKey() PrivKey {
|
||||
|
||||
// genPrivKey generates a new ed25519 private key using the provided reader.
|
||||
func genPrivKey(rand io.Reader) PrivKey {
|
||||
seed := make([]byte, SeedSize)
|
||||
|
||||
_, err := io.ReadFull(rand, seed)
|
||||
_, priv, err := ed25519.GenerateKey(rand)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return PrivKey(ed25519.NewKeyFromSeed(seed))
|
||||
return PrivKey(priv)
|
||||
}
|
||||
|
||||
// GenPrivKeyFromSecret hashes the secret with SHA2, and uses
|
||||
@@ -129,7 +149,7 @@ func GenPrivKeyFromSecret(secret []byte) PrivKey {
|
||||
|
||||
var _ crypto.PubKey = PubKey{}
|
||||
|
||||
// PubKeyEd25519 implements crypto.PubKey for the Ed25519 signature scheme.
|
||||
// PubKey implements crypto.PubKey for the Ed25519 signature scheme.
|
||||
type PubKey []byte
|
||||
|
||||
// Address is the SHA256-20 of the raw pubkey bytes.
|
||||
@@ -151,7 +171,7 @@ func (pubKey PubKey) VerifySignature(msg []byte, sig []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return ed25519.Verify(ed25519.PublicKey(pubKey), msg, sig)
|
||||
return cachingVerifier.VerifyWithOptions(ed25519.PublicKey(pubKey), msg, sig, verifyOptions)
|
||||
}
|
||||
|
||||
func (pubKey PubKey) String() string {
|
||||
@@ -169,3 +189,40 @@ func (pubKey PubKey) Equals(other crypto.PubKey) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
|
||||
// BatchVerifier implements batch verification for ed25519.
|
||||
type BatchVerifier struct {
|
||||
*ed25519.BatchVerifier
|
||||
}
|
||||
|
||||
func NewBatchVerifier() crypto.BatchVerifier {
|
||||
return &BatchVerifier{ed25519.NewBatchVerifier()}
|
||||
}
|
||||
|
||||
func (b *BatchVerifier) Add(key crypto.PubKey, msg, signature []byte) error {
|
||||
pkEd, ok := key.(PubKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("pubkey is not Ed25519")
|
||||
}
|
||||
|
||||
pkBytes := pkEd.Bytes()
|
||||
|
||||
if l := len(pkBytes); l != PubKeySize {
|
||||
return fmt.Errorf("pubkey size is incorrect; expected: %d, got %d", PubKeySize, l)
|
||||
}
|
||||
|
||||
// check that the signature is the correct length
|
||||
if len(signature) != SignatureSize {
|
||||
return errors.New("invalid signature")
|
||||
}
|
||||
|
||||
cachingVerifier.AddWithOptions(b.BatchVerifier, ed25519.PublicKey(pkBytes), msg, signature, verifyOptions)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BatchVerifier) Verify() (bool, []bool) {
|
||||
return b.BatchVerifier.Verify(crypto.CReader())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func TestSignAndValidateEd25519(t *testing.T) {
|
||||
|
||||
privKey := ed25519.GenPrivKey()
|
||||
pubKey := privKey.PubKey()
|
||||
|
||||
@@ -28,3 +27,28 @@ 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)
|
||||
}
|
||||
|
||||
ok, _ := v.Verify()
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
46
crypto/sr25519/batch.go
Normal file
46
crypto/sr25519/batch.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package sr25519
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/sr25519"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
var _ crypto.BatchVerifier = &BatchVerifier{}
|
||||
|
||||
// BatchVerifier implements batch verification for sr25519.
|
||||
type BatchVerifier struct {
|
||||
*sr25519.BatchVerifier
|
||||
}
|
||||
|
||||
func NewBatchVerifier() crypto.BatchVerifier {
|
||||
return &BatchVerifier{sr25519.NewBatchVerifier()}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
var srpk sr25519.PublicKey
|
||||
if err := srpk.UnmarshalBinary(pk); err != nil {
|
||||
return fmt.Errorf("sr25519: invalid public key: %w", err)
|
||||
}
|
||||
|
||||
var sig sr25519.Signature
|
||||
if err := sig.UnmarshalBinary(signature); err != nil {
|
||||
return fmt.Errorf("sr25519: unable to decode signature: %w", err)
|
||||
}
|
||||
|
||||
st := signingCtx.NewTranscriptBytes(msg)
|
||||
b.BatchVerifier.Add(&srpk, st, &sig)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BatchVerifier) Verify() (bool, []bool) {
|
||||
return b.BatchVerifier.Verify(crypto.CReader())
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
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 +27,42 @@ func BenchmarkVerification(b *testing.B) {
|
||||
priv := GenPrivKey()
|
||||
benchmarking.BenchmarkVerification(b, priv)
|
||||
}
|
||||
|
||||
func BenchmarkVerifyBatch(b *testing.B) {
|
||||
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()
|
||||
sig, _ := priv.Sign(msg)
|
||||
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/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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,76 +1,126 @@
|
||||
package sr25519
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/sr25519"
|
||||
|
||||
schnorrkel "github.com/ChainSafe/go-schnorrkel"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
// PrivKeySize is the number of bytes in an Sr25519 private key.
|
||||
const PrivKeySize = 32
|
||||
var (
|
||||
_ crypto.PrivKey = PrivKey{}
|
||||
|
||||
// PrivKeySr25519 implements crypto.PrivKey.
|
||||
type PrivKey []byte
|
||||
signingCtx = sr25519.NewSigningContext([]byte{})
|
||||
)
|
||||
|
||||
const (
|
||||
// PrivKeySize is the number of bytes in an Sr25519 private key.
|
||||
PrivKeySize = 32
|
||||
|
||||
KeyType = "sr25519"
|
||||
)
|
||||
|
||||
// PrivKey implements crypto.PrivKey.
|
||||
type PrivKey struct {
|
||||
msk sr25519.MiniSecretKey
|
||||
kp *sr25519.KeyPair
|
||||
}
|
||||
|
||||
// Bytes returns the byte representation of the PrivKey.
|
||||
func (privKey PrivKey) Bytes() []byte {
|
||||
return []byte(privKey)
|
||||
if privKey.kp == nil {
|
||||
return nil
|
||||
}
|
||||
return privKey.msk[:]
|
||||
}
|
||||
|
||||
// Sign produces a signature on the provided message.
|
||||
func (privKey PrivKey) Sign(msg []byte) ([]byte, error) {
|
||||
var p [PrivKeySize]byte
|
||||
copy(p[:], privKey)
|
||||
miniSecretKey, err := schnorrkel.NewMiniSecretKeyFromRaw(p)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
secretKey := miniSecretKey.ExpandEd25519()
|
||||
|
||||
signingContext := schnorrkel.NewSigningContext([]byte{}, msg)
|
||||
|
||||
sig, err := secretKey.Sign(signingContext)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
if privKey.kp == nil {
|
||||
return nil, fmt.Errorf("sr25519: uninitialized private key")
|
||||
}
|
||||
|
||||
sigBytes := sig.Encode()
|
||||
return sigBytes[:], nil
|
||||
st := signingCtx.NewTranscriptBytes(msg)
|
||||
|
||||
sig, err := privKey.kp.Sign(crypto.CReader(), st)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sr25519: failed to sign message: %w", err)
|
||||
}
|
||||
|
||||
sigBytes, err := sig.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sr25519: failed to serialize signature: %w", err)
|
||||
}
|
||||
|
||||
return sigBytes, nil
|
||||
}
|
||||
|
||||
// PubKey gets the corresponding public key from the private key.
|
||||
func (privKey PrivKey) PubKey() crypto.PubKey {
|
||||
var p [PrivKeySize]byte
|
||||
copy(p[:], privKey)
|
||||
miniSecretKey, err := schnorrkel.NewMiniSecretKeyFromRaw(p)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Invalid private key: %v", err))
|
||||
if privKey.kp == nil {
|
||||
panic("sr25519: uninitialized private key")
|
||||
}
|
||||
secretKey := miniSecretKey.ExpandEd25519()
|
||||
|
||||
pubkey, err := secretKey.Public()
|
||||
b, err := privKey.kp.PublicKey().MarshalBinary()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not generate public key: %v", err))
|
||||
panic("sr25519: failed to serialize public key: " + err.Error())
|
||||
}
|
||||
key := pubkey.Encode()
|
||||
return PubKey(key[:])
|
||||
|
||||
return PubKey(b)
|
||||
}
|
||||
|
||||
// Equals - you probably don't need to use this.
|
||||
// Runs in constant time based on length of the keys.
|
||||
func (privKey PrivKey) Equals(other crypto.PrivKey) bool {
|
||||
if otherEd, ok := other.(PrivKey); ok {
|
||||
return subtle.ConstantTimeCompare(privKey[:], otherEd[:]) == 1
|
||||
if otherSr, ok := other.(PrivKey); ok {
|
||||
return privKey.msk.Equal(&otherSr.msk)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (privKey PrivKey) Type() string {
|
||||
return keyType
|
||||
return KeyType
|
||||
}
|
||||
|
||||
func (privKey PrivKey) MarshalJSON() ([]byte, error) {
|
||||
var b []byte
|
||||
|
||||
// Handle uninitialized private keys gracefully.
|
||||
if privKey.kp != nil {
|
||||
b = privKey.Bytes()
|
||||
}
|
||||
|
||||
return json.Marshal(b)
|
||||
}
|
||||
|
||||
func (privKey *PrivKey) UnmarshalJSON(data []byte) error {
|
||||
for i := range privKey.msk {
|
||||
privKey.msk[i] = 0
|
||||
}
|
||||
privKey.kp = nil
|
||||
|
||||
var b []byte
|
||||
if err := json.Unmarshal(data, &b); err != nil {
|
||||
return fmt.Errorf("sr25519: failed to deserialize JSON: %w", err)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
msk, err := sr25519.NewMiniSecretKeyFromBytes(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sk := msk.ExpandEd25519()
|
||||
|
||||
privKey.msk = *msk
|
||||
privKey.kp = sk.KeyPair()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenPrivKey generates a new sr25519 private key.
|
||||
@@ -81,19 +131,18 @@ func GenPrivKey() PrivKey {
|
||||
}
|
||||
|
||||
// genPrivKey generates a new sr25519 private key using the provided reader.
|
||||
func genPrivKey(rand io.Reader) PrivKey {
|
||||
var seed [64]byte
|
||||
|
||||
out := make([]byte, 64)
|
||||
_, err := io.ReadFull(rand, out)
|
||||
func genPrivKey(rng io.Reader) PrivKey {
|
||||
msk, err := sr25519.GenerateMiniSecretKey(rng)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
panic("sr25519: failed to generate MiniSecretKey: " + err.Error())
|
||||
}
|
||||
|
||||
copy(seed[:], out)
|
||||
sk := msk.ExpandEd25519()
|
||||
|
||||
key := schnorrkel.NewMiniSecretKey(seed).ExpandEd25519().Encode()
|
||||
return key[:]
|
||||
return PrivKey{
|
||||
msk: *msk,
|
||||
kp: sk.KeyPair(),
|
||||
}
|
||||
}
|
||||
|
||||
// GenPrivKeyFromSecret hashes the secret with SHA2, and uses
|
||||
@@ -102,9 +151,14 @@ func genPrivKey(rand io.Reader) PrivKey {
|
||||
// if it's derived from user input.
|
||||
func GenPrivKeyFromSecret(secret []byte) PrivKey {
|
||||
seed := crypto.Sha256(secret) // Not Ripemd160 because we want 32 bytes.
|
||||
var bz [PrivKeySize]byte
|
||||
copy(bz[:], seed)
|
||||
privKey, _ := schnorrkel.NewMiniSecretKeyFromRaw(bz)
|
||||
key := privKey.ExpandEd25519().Encode()
|
||||
return key[:]
|
||||
|
||||
var privKey PrivKey
|
||||
if err := privKey.msk.UnmarshalBinary(seed); err != nil {
|
||||
panic("sr25519: failed to deserialize MiniSecretKey: " + err.Error())
|
||||
}
|
||||
|
||||
sk := privKey.msk.ExpandEd25519()
|
||||
privKey.kp = sk.KeyPair()
|
||||
|
||||
return privKey
|
||||
}
|
||||
|
||||
@@ -4,25 +4,30 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/sr25519"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
|
||||
schnorrkel "github.com/ChainSafe/go-schnorrkel"
|
||||
)
|
||||
|
||||
var _ crypto.PubKey = PubKey{}
|
||||
|
||||
// PubKeySize is the number of bytes in an Sr25519 public key.
|
||||
const (
|
||||
// PubKeySize is the number of bytes in an Sr25519 public key.
|
||||
PubKeySize = 32
|
||||
keyType = "sr25519"
|
||||
|
||||
// SignatureSize is the size of a Sr25519 signature in bytes.
|
||||
SignatureSize = 64
|
||||
)
|
||||
|
||||
// PubKeySr25519 implements crypto.PubKey for the Sr25519 signature scheme.
|
||||
// PubKey implements crypto.PubKey for the Sr25519 signature scheme.
|
||||
type PubKey []byte
|
||||
|
||||
// Address is the SHA256-20 of the raw pubkey bytes.
|
||||
func (pubKey PubKey) Address() crypto.Address {
|
||||
if len(pubKey) != PubKeySize {
|
||||
panic("pubkey is incorrect size")
|
||||
}
|
||||
return crypto.Address(tmhash.SumTruncated(pubKey[:]))
|
||||
}
|
||||
|
||||
@@ -31,47 +36,35 @@ func (pubKey PubKey) Bytes() []byte {
|
||||
return []byte(pubKey)
|
||||
}
|
||||
|
||||
func (pubKey PubKey) VerifySignature(msg []byte, sig []byte) bool {
|
||||
// make sure we use the same algorithm to sign
|
||||
if len(sig) != SignatureSize {
|
||||
return false
|
||||
// Equals - checks that two public keys are the same time
|
||||
// Runs in constant time based on length of the keys.
|
||||
func (pubKey PubKey) Equals(other crypto.PubKey) bool {
|
||||
if otherSr, ok := other.(PubKey); ok {
|
||||
return bytes.Equal(pubKey[:], otherSr[:])
|
||||
}
|
||||
var sig64 [SignatureSize]byte
|
||||
copy(sig64[:], sig)
|
||||
|
||||
publicKey := &(schnorrkel.PublicKey{})
|
||||
var p [PubKeySize]byte
|
||||
copy(p[:], pubKey)
|
||||
err := publicKey.Decode(p)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
func (pubKey PubKey) VerifySignature(msg []byte, sigBytes []byte) bool {
|
||||
var srpk sr25519.PublicKey
|
||||
if err := srpk.UnmarshalBinary(pubKey); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
signingContext := schnorrkel.NewSigningContext([]byte{}, msg)
|
||||
|
||||
signature := &(schnorrkel.Signature{})
|
||||
err = signature.Decode(sig64)
|
||||
if err != nil {
|
||||
var sig sr25519.Signature
|
||||
if err := sig.UnmarshalBinary(sigBytes); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return publicKey.Verify(signature, signingContext)
|
||||
st := signingCtx.NewTranscriptBytes(msg)
|
||||
return srpk.Verify(st, &sig)
|
||||
}
|
||||
|
||||
func (pubKey PubKey) String() string {
|
||||
return fmt.Sprintf("PubKeySr25519{%X}", []byte(pubKey))
|
||||
}
|
||||
|
||||
// Equals - checks that two public keys are the same time
|
||||
// Runs in constant time based on length of the keys.
|
||||
func (pubKey PubKey) Equals(other crypto.PubKey) bool {
|
||||
if otherEd, ok := other.(PubKey); ok {
|
||||
return bytes.Equal(pubKey[:], otherEd[:])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (pubKey PubKey) Type() string {
|
||||
return keyType
|
||||
|
||||
return KeyType
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package sr25519_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -11,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
func TestSignAndValidateSr25519(t *testing.T) {
|
||||
|
||||
privKey := sr25519.GenPrivKey()
|
||||
pubKey := privKey.PubKey()
|
||||
|
||||
@@ -29,3 +30,69 @@ func TestSignAndValidateSr25519(t *testing.T) {
|
||||
|
||||
assert.False(t, pubKey.VerifySignature(msg, sig))
|
||||
}
|
||||
|
||||
func TestBatchSafe(t *testing.T) {
|
||||
v := sr25519.NewBatchVerifier()
|
||||
vFail := 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)
|
||||
|
||||
switch i % 2 {
|
||||
case 0:
|
||||
err = vFail.Add(pub, msg, sig)
|
||||
case 1:
|
||||
msg[2] ^= byte(0x01)
|
||||
err = vFail.Add(pub, msg, sig)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
ok, valid := v.Verify()
|
||||
require.True(t, ok, "failed batch verification")
|
||||
for i, ok := range valid {
|
||||
require.Truef(t, ok, "sig[%d] should be marked valid", i)
|
||||
}
|
||||
|
||||
ok, valid = vFail.Verify()
|
||||
require.False(t, ok, "succeeded batch verification (invalid batch)")
|
||||
for i, ok := range valid {
|
||||
expected := (i % 2) == 0
|
||||
require.Equalf(t, expected, ok, "sig[%d] should be %v", i, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
privKey := sr25519.GenPrivKey()
|
||||
|
||||
t.Run("PrivKey", func(t *testing.T) {
|
||||
b, err := json.Marshal(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// b should be the base64 encoded MiniSecretKey, enclosed by doublequotes.
|
||||
b64 := base64.StdEncoding.EncodeToString(privKey.Bytes())
|
||||
b64 = "\"" + b64 + "\""
|
||||
require.Equal(t, []byte(b64), b)
|
||||
|
||||
var privKey2 sr25519.PrivKey
|
||||
err = json.Unmarshal(b, &privKey2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, privKey2.Bytes(), sr25519.PrivKeySize)
|
||||
require.EqualValues(t, privKey.Bytes(), privKey2.Bytes())
|
||||
})
|
||||
|
||||
// PubKeys are just []byte, so there is no special handling.
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -4,7 +4,6 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.0
|
||||
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d
|
||||
github.com/adlio/schema v1.3.3
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
@@ -15,7 +14,6 @@ require (
|
||||
github.com/golangci/golangci-lint v1.49.0
|
||||
github.com/google/orderedcode v0.0.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gtank/merlin v0.1.1
|
||||
github.com/informalsystems/tm-load-test v1.0.0
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/libp2p/go-buffer-pool v0.1.0
|
||||
@@ -51,6 +49,7 @@ require (
|
||||
github.com/cosmos/gogoproto v1.4.2
|
||||
github.com/gofrs/uuid v4.3.0+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae
|
||||
github.com/vektra/mockery/v2 v2.14.0
|
||||
gonum.org/v1/gonum v0.12.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
@@ -87,7 +86,6 @@ require (
|
||||
github.com/containerd/containerd v1.6.8 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/containerd/typeurl v1.0.2 // indirect
|
||||
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/curioswitch/go-reassign v0.1.2 // indirect
|
||||
github.com/daixiang0/gci v0.6.3 // indirect
|
||||
@@ -143,7 +141,6 @@ require (
|
||||
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/gtank/ristretto255 v0.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
@@ -178,7 +175,6 @@ require (
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
||||
github.com/mgechev/revive v1.2.3 // indirect
|
||||
github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/buildkit v0.10.3 // indirect
|
||||
|
||||
13
go.sum
13
go.sum
@@ -56,8 +56,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
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/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
@@ -547,11 +545,6 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
|
||||
github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
@@ -746,9 +739,6 @@ github.com/mgechev/revive v1.2.3/go.mod h1:iAWlQishqCuj4yhV24FTnKSXGpbAA+0SckXB8
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
@@ -812,6 +802,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae h1:FatpGJD2jmJfhZiFDElaC0QhZUDQnxUeAwTGkfAHN3I=
|
||||
github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@@ -1210,7 +1202,6 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
gogotypes "github.com/cosmos/gogoproto/types"
|
||||
"github.com/gtank/merlin"
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/merlin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
|
||||
@@ -208,9 +208,7 @@ func (c *evilConn) signChallenge() []byte {
|
||||
|
||||
const challengeSize = 32
|
||||
var challenge [challengeSize]byte
|
||||
challengeSlice := transcript.ExtractBytes(labelSecretConnectionMac, challengeSize)
|
||||
|
||||
copy(challenge[:], challengeSlice[0:challengeSize])
|
||||
transcript.ExtractBytes(challenge[:], labelSecretConnectionMac)
|
||||
|
||||
sendAead, err := chacha20poly1305.New(sendSecret[:])
|
||||
if err != nil {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"time"
|
||||
|
||||
gogotypes "github.com/cosmos/gogoproto/types"
|
||||
"github.com/gtank/merlin"
|
||||
pool "github.com/libp2p/go-buffer-pool"
|
||||
"github.com/oasisprotocol/curve25519-voi/primitives/merlin"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
@@ -38,16 +38,16 @@ const (
|
||||
aeadSizeOverhead = 16 // overhead of poly 1305 authentication tag
|
||||
aeadKeySize = chacha20poly1305.KeySize
|
||||
aeadNonceSize = chacha20poly1305.NonceSize
|
||||
|
||||
labelEphemeralLowerPublicKey = "EPHEMERAL_LOWER_PUBLIC_KEY"
|
||||
labelEphemeralUpperPublicKey = "EPHEMERAL_UPPER_PUBLIC_KEY"
|
||||
labelDHSecret = "DH_SECRET"
|
||||
labelSecretConnectionMac = "SECRET_CONNECTION_MAC"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSmallOrderRemotePubKey = errors.New("detected low order point from remote peer")
|
||||
|
||||
labelEphemeralLowerPublicKey = []byte("EPHEMERAL_LOWER_PUBLIC_KEY")
|
||||
labelEphemeralUpperPublicKey = []byte("EPHEMERAL_UPPER_PUBLIC_KEY")
|
||||
labelDHSecret = []byte("DH_SECRET")
|
||||
labelSecretConnectionMac = []byte("SECRET_CONNECTION_MAC")
|
||||
|
||||
secretConnKeyAndChallengeGen = []byte("TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN")
|
||||
)
|
||||
|
||||
@@ -132,9 +132,7 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*
|
||||
|
||||
const challengeSize = 32
|
||||
var challenge [challengeSize]byte
|
||||
challengeSlice := transcript.ExtractBytes(labelSecretConnectionMac, challengeSize)
|
||||
|
||||
copy(challenge[:], challengeSlice[0:challengeSize])
|
||||
transcript.ExtractBytes(challenge[:], labelSecretConnectionMac)
|
||||
|
||||
sendAead, err := chacha20poly1305.New(sendSecret[:])
|
||||
if err != nil {
|
||||
|
||||
@@ -1,30 +1,132 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/batch"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
)
|
||||
|
||||
// ValidateTime does a basic time validation ensuring time does not drift too
|
||||
// much: +/- one year.
|
||||
// TODO: reduce this to eg 1 day
|
||||
// NOTE: DO NOT USE in ValidateBasic methods in this package. This function
|
||||
// can only be used for real time validation, like on proposals and votes
|
||||
// in the consensus. If consensus is stuck, and rounds increase for more than a day,
|
||||
// having only a 1-day band here could break things...
|
||||
// Can't use for validating blocks because we may be syncing years worth of history.
|
||||
func ValidateTime(t time.Time) error {
|
||||
var (
|
||||
now = tmtime.Now()
|
||||
oneYear = 8766 * time.Hour
|
||||
)
|
||||
if t.Before(now.Add(-oneYear)) || t.After(now.Add(oneYear)) {
|
||||
return fmt.Errorf("time drifted too much. Expected: -1 < %v < 1 year", now)
|
||||
const batchVerifyThreshold = 2
|
||||
|
||||
func shouldBatchVerify(vals *ValidatorSet, commit *Commit) bool {
|
||||
return len(commit.Signatures) >= batchVerifyThreshold && batch.SupportsBatchVerifier(vals.GetProposer().PubKey)
|
||||
}
|
||||
|
||||
// VerifyCommit verifies +2/3 of the set had signed the given commit.
|
||||
//
|
||||
// It checks all the signatures! While it's safe to exit as soon as we have
|
||||
// 2/3+ signatures, doing so would impact incentivization logic in the ABCI
|
||||
// application that depends on the LastCommitInfo sent in BeginBlock, which
|
||||
// includes which validators signed. For instance, Gaia incentivizes proposers
|
||||
// with a bonus for including more than +2/3 of the signatures.
|
||||
func VerifyCommit(chainID string, vals *ValidatorSet, blockID BlockID,
|
||||
height int64, commit *Commit) error {
|
||||
// run a basic validation of the arguments
|
||||
if err := verifyBasicValsAndCommit(vals, commit, height, blockID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
// calculate voting power needed. Note that total voting power is capped to
|
||||
// 1/8th of max int64 so this operation should never overflow
|
||||
votingPowerNeeded := vals.TotalVotingPower() * 2 / 3
|
||||
|
||||
// ignore all absent signatures
|
||||
ignore := func(c CommitSig) bool { return c.Absent() }
|
||||
|
||||
// only count the signatures that are for the block
|
||||
count := func(c CommitSig) bool { return c.ForBlock() }
|
||||
|
||||
// attempt to batch verify
|
||||
if shouldBatchVerify(vals, commit) {
|
||||
return verifyCommitBatch(chainID, vals, commit,
|
||||
votingPowerNeeded, ignore, count, true, true)
|
||||
}
|
||||
|
||||
// if verification failed or is not supported then fallback to single verification
|
||||
return verifyCommitSingle(chainID, vals, commit, votingPowerNeeded,
|
||||
ignore, count, true, true)
|
||||
}
|
||||
|
||||
// LIGHT CLIENT VERIFICATION METHODS
|
||||
|
||||
// VerifyCommitLight verifies +2/3 of the set had signed the given commit.
|
||||
//
|
||||
// This method is primarily used by the light client and does not check all the
|
||||
// signatures.
|
||||
func VerifyCommitLight(chainID string, vals *ValidatorSet, blockID BlockID,
|
||||
height int64, commit *Commit) error {
|
||||
// run a basic validation of the arguments
|
||||
if err := verifyBasicValsAndCommit(vals, commit, height, blockID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// calculate voting power needed
|
||||
votingPowerNeeded := vals.TotalVotingPower() * 2 / 3
|
||||
|
||||
// ignore all commit signatures that are not for the block
|
||||
ignore := func(c CommitSig) bool { return !c.ForBlock() }
|
||||
|
||||
// count all the remaining signatures
|
||||
count := func(c CommitSig) bool { return true }
|
||||
|
||||
// attempt to batch verify
|
||||
if shouldBatchVerify(vals, commit) {
|
||||
return verifyCommitBatch(chainID, vals, commit,
|
||||
votingPowerNeeded, ignore, count, false, true)
|
||||
}
|
||||
|
||||
// if verification failed or is not supported then fallback to single verification
|
||||
return verifyCommitSingle(chainID, vals, commit, votingPowerNeeded,
|
||||
ignore, count, false, true)
|
||||
}
|
||||
|
||||
// VerifyCommitLightTrusting verifies that trustLevel of the validator set signed
|
||||
// this commit.
|
||||
//
|
||||
// NOTE the given validators do not necessarily correspond to the validator set
|
||||
// for this commit, but there may be some intersection.
|
||||
//
|
||||
// This method is primarily used by the light client and does not check all the
|
||||
// signatures.
|
||||
func VerifyCommitLightTrusting(chainID string, vals *ValidatorSet, commit *Commit, trustLevel tmmath.Fraction) error {
|
||||
// sanity checks
|
||||
if vals == nil {
|
||||
return errors.New("nil validator set")
|
||||
}
|
||||
if trustLevel.Denominator == 0 {
|
||||
return errors.New("trustLevel has zero Denominator")
|
||||
}
|
||||
if commit == nil {
|
||||
return errors.New("nil commit")
|
||||
}
|
||||
|
||||
// safely calculate voting power needed.
|
||||
totalVotingPowerMulByNumerator, overflow := safeMul(vals.TotalVotingPower(), int64(trustLevel.Numerator))
|
||||
if overflow {
|
||||
return errors.New("int64 overflow while calculating voting power needed. please provide smaller trustLevel numerator")
|
||||
}
|
||||
votingPowerNeeded := totalVotingPowerMulByNumerator / int64(trustLevel.Denominator)
|
||||
|
||||
// ignore all commit signatures that are not for the block
|
||||
ignore := func(c CommitSig) bool { return !c.ForBlock() }
|
||||
|
||||
// count all the remaining signatures
|
||||
count := func(c CommitSig) bool { return true }
|
||||
|
||||
// attempt to batch verify commit. As the validator set doesn't necessarily
|
||||
// correspond with the validator set that signed the block we need to look
|
||||
// up by address rather than index.
|
||||
if shouldBatchVerify(vals, commit) {
|
||||
return verifyCommitBatch(chainID, vals, commit,
|
||||
votingPowerNeeded, ignore, count, false, false)
|
||||
}
|
||||
|
||||
// attempt with single verification
|
||||
return verifyCommitSingle(chainID, vals, commit, votingPowerNeeded,
|
||||
ignore, count, false, false)
|
||||
}
|
||||
|
||||
// ValidateHash returns an error if the hash is not empty, but its
|
||||
@@ -38,3 +140,218 @@ func ValidateHash(h []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Batch verification
|
||||
|
||||
// verifyCommitBatch batch verifies commits. This routine is equivalent
|
||||
// to verifyCommitSingle in behavior, just faster iff every signature in the
|
||||
// batch is valid.
|
||||
//
|
||||
// Note: The caller is responsible for checking to see if this routine is
|
||||
// usable via `shouldVerifyBatch(vals, commit)`.
|
||||
func verifyCommitBatch(
|
||||
chainID string,
|
||||
vals *ValidatorSet,
|
||||
commit *Commit,
|
||||
votingPowerNeeded int64,
|
||||
ignoreSig func(CommitSig) bool,
|
||||
countSig func(CommitSig) bool,
|
||||
countAllSignatures bool,
|
||||
lookUpByIndex bool,
|
||||
) error {
|
||||
var (
|
||||
val *Validator
|
||||
valIdx int32
|
||||
seenVals = make(map[int32]int, len(commit.Signatures))
|
||||
batchSigIdxs = make([]int, 0, len(commit.Signatures))
|
||||
talliedVotingPower int64
|
||||
)
|
||||
// attempt to create a batch verifier
|
||||
bv, ok := batch.CreateBatchVerifier(vals.GetProposer().PubKey)
|
||||
// re-check if batch verification is supported
|
||||
if !ok || len(commit.Signatures) < batchVerifyThreshold {
|
||||
// This should *NEVER* happen.
|
||||
return fmt.Errorf("unsupported signature algorithm or insufficient signatures for batch verification")
|
||||
}
|
||||
|
||||
for idx, commitSig := range commit.Signatures {
|
||||
// skip over signatures that should be ignored
|
||||
if ignoreSig(commitSig) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the vals and commit have a 1-to-1 correspondance we can retrieve
|
||||
// them by index else we need to retrieve them by address
|
||||
if lookUpByIndex {
|
||||
val = vals.Validators[idx]
|
||||
} else {
|
||||
valIdx, val = vals.GetByAddress(commitSig.ValidatorAddress)
|
||||
|
||||
// if the signature doesn't belong to anyone in the validator set
|
||||
// then we just skip over it
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// because we are getting validators by address we need to make sure
|
||||
// that the same validator doesn't commit twice
|
||||
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))
|
||||
|
||||
// add the key, sig and message to the verifier
|
||||
if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
|
||||
return err
|
||||
}
|
||||
batchSigIdxs = append(batchSigIdxs, idx)
|
||||
|
||||
// If this signature counts then add the voting power of the validator
|
||||
// to the tally
|
||||
if countSig(commitSig) {
|
||||
talliedVotingPower += val.VotingPower
|
||||
}
|
||||
|
||||
// if we don't need to verify all signatures and already have sufficient
|
||||
// voting power we can break from batching and verify all the signatures
|
||||
if !countAllSignatures && talliedVotingPower > votingPowerNeeded {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that we have batched together enough signatures to exceed the
|
||||
// voting power needed else there is no need to even verify
|
||||
if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed {
|
||||
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
|
||||
}
|
||||
|
||||
// attempt to verify the batch.
|
||||
ok, validSigs := bv.Verify()
|
||||
if ok {
|
||||
// success
|
||||
return nil
|
||||
}
|
||||
|
||||
// one or more of the signatures is invalid, find and return the first
|
||||
// invalid signature.
|
||||
for i, ok := range validSigs {
|
||||
if !ok {
|
||||
// go back from the batch index to the commit.Signatures index
|
||||
idx := batchSigIdxs[i]
|
||||
sig := commit.Signatures[idx]
|
||||
return fmt.Errorf("wrong signature (#%d): %X", idx, sig)
|
||||
}
|
||||
}
|
||||
|
||||
// execution reaching here is a bug, and one of the following has
|
||||
// happened:
|
||||
// * non-zero tallied voting power, empty batch (impossible?)
|
||||
// * bv.Verify() returned `false, []bool{true, ..., true}` (BUG)
|
||||
return fmt.Errorf("BUG: batch verification failed with no invalid signatures")
|
||||
}
|
||||
|
||||
// Single Verification
|
||||
|
||||
// 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.
|
||||
// CONTRACT: both commit and validator set should have passed validate basic
|
||||
func verifyCommitSingle(
|
||||
chainID string,
|
||||
vals *ValidatorSet,
|
||||
commit *Commit,
|
||||
votingPowerNeeded int64,
|
||||
ignoreSig func(CommitSig) bool,
|
||||
countSig func(CommitSig) bool,
|
||||
countAllSignatures bool,
|
||||
lookUpByIndex bool,
|
||||
) error {
|
||||
var (
|
||||
val *Validator
|
||||
valIdx int32
|
||||
seenVals = make(map[int32]int, len(commit.Signatures))
|
||||
talliedVotingPower int64
|
||||
voteSignBytes []byte
|
||||
)
|
||||
for idx, commitSig := range commit.Signatures {
|
||||
if ignoreSig(commitSig) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the vals and commit have a 1-to-1 correspondance we can retrieve
|
||||
// them by index else we need to retrieve them by address
|
||||
if lookUpByIndex {
|
||||
val = vals.Validators[idx]
|
||||
} else {
|
||||
valIdx, val = vals.GetByAddress(commitSig.ValidatorAddress)
|
||||
|
||||
// if the signature doesn't belong to anyone in the validator set
|
||||
// then we just skip over it
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// because we are getting validators by address we need to make sure
|
||||
// that the same validator doesn't commit twice
|
||||
if firstIndex, ok := seenVals[valIdx]; ok {
|
||||
secondIndex := idx
|
||||
return fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
|
||||
}
|
||||
seenVals[valIdx] = idx
|
||||
}
|
||||
|
||||
voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))
|
||||
|
||||
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
|
||||
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
|
||||
}
|
||||
|
||||
// If this signature counts then add the voting power of the validator
|
||||
// to the tally
|
||||
if countSig(commitSig) {
|
||||
talliedVotingPower += val.VotingPower
|
||||
}
|
||||
|
||||
// check if we have enough signatures and can thus exit early
|
||||
if !countAllSignatures && talliedVotingPower > votingPowerNeeded {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed {
|
||||
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyBasicValsAndCommit(vals *ValidatorSet, commit *Commit, height int64, blockID BlockID) error {
|
||||
if vals == nil {
|
||||
return errors.New("nil validator set")
|
||||
}
|
||||
|
||||
if commit == nil {
|
||||
return errors.New("nil commit")
|
||||
}
|
||||
|
||||
if vals.Size() != len(commit.Signatures) {
|
||||
return NewErrInvalidCommitSignatures(vals.Size(), len(commit.Signatures))
|
||||
}
|
||||
|
||||
// Validate Height and BlockID.
|
||||
if height != commit.Height {
|
||||
return NewErrInvalidCommitHeight(height, commit.Height)
|
||||
}
|
||||
if !blockID.Equals(commit.BlockID) {
|
||||
return fmt.Errorf("invalid commit -- wrong block ID: want %v, got %v",
|
||||
blockID, commit.BlockID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
261
types/validation_test.go
Normal file
261
types/validation_test.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
// Check VerifyCommit, VerifyCommitLight and VerifyCommitLightTrusting basic
|
||||
// verification.
|
||||
func TestValidatorSet_VerifyCommit_All(t *testing.T) {
|
||||
var (
|
||||
round = int32(0)
|
||||
height = int64(100)
|
||||
|
||||
blockID = makeBlockID([]byte("blockhash"), 1000, []byte("partshash"))
|
||||
chainID = "Lalande21185"
|
||||
trustLevel = tmmath.Fraction{Numerator: 2, Denominator: 3}
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
// vote chainID
|
||||
chainID string
|
||||
// vote blockID
|
||||
blockID BlockID
|
||||
valSize int
|
||||
|
||||
// height of the commit
|
||||
height int64
|
||||
|
||||
// votes
|
||||
blockVotes int
|
||||
nilVotes int
|
||||
absentVotes int
|
||||
|
||||
expErr bool
|
||||
}{
|
||||
{"good (batch verification)", chainID, blockID, 3, height, 3, 0, 0, false},
|
||||
{"good (single verification)", chainID, blockID, 1, height, 1, 0, 0, false},
|
||||
|
||||
{"wrong signature (#0)", "EpsilonEridani", blockID, 2, height, 2, 0, 0, true},
|
||||
{"wrong block ID", chainID, makeBlockIDRandom(), 2, height, 2, 0, 0, true},
|
||||
{"wrong height", chainID, blockID, 1, height - 1, 1, 0, 0, true},
|
||||
|
||||
{"wrong set size: 4 vs 3", chainID, blockID, 4, height, 3, 0, 0, true},
|
||||
{"wrong set size: 1 vs 2", chainID, blockID, 1, height, 2, 0, 0, true},
|
||||
|
||||
{"insufficient voting power: got 30, needed more than 66", chainID, blockID, 10, height, 3, 2, 5, true},
|
||||
{"insufficient voting power: got 0, needed more than 6", chainID, blockID, 1, height, 0, 0, 1, true},
|
||||
{"insufficient voting power: got 60, needed more than 60", chainID, blockID, 9, height, 6, 3, 0, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
_, valSet, vals := randVoteSet(tc.height, round, tmproto.PrecommitType, tc.valSize, 10)
|
||||
totalVotes := tc.blockVotes + tc.absentVotes + tc.nilVotes
|
||||
sigs := make([]CommitSig, totalVotes)
|
||||
vi := 0
|
||||
// add absent sigs first
|
||||
for i := 0; i < tc.absentVotes; i++ {
|
||||
sigs[vi] = NewCommitSigAbsent()
|
||||
vi++
|
||||
}
|
||||
for i := 0; i < tc.blockVotes+tc.nilVotes; i++ {
|
||||
|
||||
pubKey, err := vals[vi%len(vals)].GetPubKey()
|
||||
require.NoError(t, err)
|
||||
vote := &Vote{
|
||||
ValidatorAddress: pubKey.Address(),
|
||||
ValidatorIndex: int32(vi),
|
||||
Height: tc.height,
|
||||
Round: round,
|
||||
Type: tmproto.PrecommitType,
|
||||
BlockID: tc.blockID,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if i >= tc.blockVotes {
|
||||
vote.BlockID = BlockID{}
|
||||
}
|
||||
|
||||
v := vote.ToProto()
|
||||
|
||||
require.NoError(t, vals[vi%len(vals)].SignVote(tc.chainID, v))
|
||||
vote.Signature = v.Signature
|
||||
|
||||
sigs[vi] = vote.CommitSig()
|
||||
|
||||
vi++
|
||||
}
|
||||
commit := NewCommit(tc.height, round, tc.blockID, sigs)
|
||||
|
||||
err := valSet.VerifyCommit(chainID, blockID, height, commit)
|
||||
if tc.expErr {
|
||||
if assert.Error(t, err, "VerifyCommit") {
|
||||
assert.Contains(t, err.Error(), tc.description, "VerifyCommit")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "VerifyCommit")
|
||||
}
|
||||
|
||||
err = valSet.VerifyCommitLight(chainID, blockID, height, commit)
|
||||
if tc.expErr {
|
||||
if assert.Error(t, err, "VerifyCommitLight") {
|
||||
assert.Contains(t, err.Error(), tc.description, "VerifyCommitLight")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "VerifyCommitLight")
|
||||
}
|
||||
|
||||
// only a subsection of the tests apply to VerifyCommitLightTrusting
|
||||
if totalVotes != tc.valSize || !tc.blockID.Equals(blockID) || tc.height != height {
|
||||
tc.expErr = false
|
||||
}
|
||||
err = valSet.VerifyCommitLightTrusting(chainID, commit, trustLevel)
|
||||
if tc.expErr {
|
||||
if assert.Error(t, err, "VerifyCommitLightTrusting") {
|
||||
assert.Contains(t, err.Error(), tc.description, "VerifyCommitLightTrusting")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "VerifyCommitLightTrusting")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommit_CheckAllSignatures(t *testing.T) {
|
||||
var (
|
||||
chainID = "test_chain_id"
|
||||
h = int64(3)
|
||||
blockID = makeBlockIDRandom()
|
||||
)
|
||||
|
||||
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
|
||||
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
|
||||
|
||||
// malleate 4th signature
|
||||
vote := voteSet.GetByIndex(3)
|
||||
v := vote.ToProto()
|
||||
err = vals[3].SignVote("CentaurusA", v)
|
||||
require.NoError(t, err)
|
||||
vote.Signature = v.Signature
|
||||
commit.Signatures[3] = vote.CommitSig()
|
||||
|
||||
err = valSet.VerifyCommit(chainID, blockID, h, commit)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong signature (#3)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLight_ReturnsAsSoonAsMajorityOfVotingPowerSigned(t *testing.T) {
|
||||
var (
|
||||
chainID = "test_chain_id"
|
||||
h = int64(3)
|
||||
blockID = makeBlockIDRandom()
|
||||
)
|
||||
|
||||
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
|
||||
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
|
||||
|
||||
// malleate 4th signature (3 signatures are enough for 2/3+)
|
||||
vote := voteSet.GetByIndex(3)
|
||||
v := vote.ToProto()
|
||||
err = vals[3].SignVote("CentaurusA", v)
|
||||
require.NoError(t, err)
|
||||
vote.Signature = v.Signature
|
||||
commit.Signatures[3] = vote.CommitSig()
|
||||
|
||||
err = valSet.VerifyCommitLight(chainID, blockID, h, commit)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLightTrusting_ReturnsAsSoonAsTrustLevelOfVotingPowerSigned(t *testing.T) {
|
||||
var (
|
||||
chainID = "test_chain_id"
|
||||
h = int64(3)
|
||||
blockID = makeBlockIDRandom()
|
||||
)
|
||||
|
||||
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
|
||||
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
|
||||
|
||||
// malleate 3rd signature (2 signatures are enough for 1/3+ trust level)
|
||||
vote := voteSet.GetByIndex(2)
|
||||
v := vote.ToProto()
|
||||
err = vals[2].SignVote("CentaurusA", v)
|
||||
require.NoError(t, err)
|
||||
vote.Signature = v.Signature
|
||||
commit.Signatures[2] = vote.CommitSig()
|
||||
|
||||
err = valSet.VerifyCommitLightTrusting(chainID, commit, tmmath.Fraction{Numerator: 1, Denominator: 3})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLightTrusting(t *testing.T) {
|
||||
var (
|
||||
blockID = makeBlockIDRandom()
|
||||
voteSet, originalValset, vals = randVoteSet(1, 1, tmproto.PrecommitType, 6, 1)
|
||||
commit, err = MakeCommit(blockID, 1, 1, voteSet, vals, time.Now())
|
||||
newValSet, _ = RandValidatorSet(2, 1)
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
valSet *ValidatorSet
|
||||
err bool
|
||||
}{
|
||||
// good
|
||||
0: {
|
||||
valSet: originalValset,
|
||||
err: false,
|
||||
},
|
||||
// bad - no overlap between validator sets
|
||||
1: {
|
||||
valSet: newValSet,
|
||||
err: true,
|
||||
},
|
||||
// good - first two are different but the rest of the same -> >1/3
|
||||
2: {
|
||||
valSet: NewValidatorSet(append(newValSet.Validators, originalValset.Validators...)),
|
||||
err: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err = tc.valSet.VerifyCommitLightTrusting("test_chain_id", commit,
|
||||
tmmath.Fraction{Numerator: 1, Denominator: 3})
|
||||
if tc.err {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLightTrustingErrorsOnOverflow(t *testing.T) {
|
||||
var (
|
||||
blockID = makeBlockIDRandom()
|
||||
voteSet, valSet, vals = randVoteSet(1, 1, tmproto.PrecommitType, 1, MaxTotalVotingPower)
|
||||
commit, err = MakeCommit(blockID, 1, 1, voteSet, vals, time.Now())
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = valSet.VerifyCommitLightTrusting("test_chain_id", commit,
|
||||
tmmath.Fraction{Numerator: 25, Denominator: 55})
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "int64 overflow")
|
||||
}
|
||||
}
|
||||
@@ -657,172 +657,25 @@ func (vals *ValidatorSet) UpdateWithChangeSet(changes []*Validator) error {
|
||||
return vals.updateWithChangeSet(changes, true)
|
||||
}
|
||||
|
||||
// VerifyCommit verifies +2/3 of the set had signed the given commit.
|
||||
//
|
||||
// It checks all the signatures! While it's safe to exit as soon as we have
|
||||
// 2/3+ signatures, doing so would impact incentivization logic in the ABCI
|
||||
// application that depends on the LastCommitInfo sent in BeginBlock, which
|
||||
// includes which validators signed. For instance, Gaia incentivizes proposers
|
||||
// with a bonus for including more than +2/3 of the signatures.
|
||||
// VerifyCommit verifies +2/3 of the set had signed the given commit and all
|
||||
// other signatures are valid
|
||||
func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
|
||||
height int64, commit *Commit) error {
|
||||
|
||||
if vals.Size() != len(commit.Signatures) {
|
||||
return NewErrInvalidCommitSignatures(vals.Size(), len(commit.Signatures))
|
||||
}
|
||||
|
||||
// Validate Height and BlockID.
|
||||
if height != commit.Height {
|
||||
return NewErrInvalidCommitHeight(height, commit.Height)
|
||||
}
|
||||
if !blockID.Equals(commit.BlockID) {
|
||||
return fmt.Errorf("invalid commit -- wrong block ID: want %v, got %v",
|
||||
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.
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// Good!
|
||||
if commitSig.ForBlock() {
|
||||
talliedVotingPower += val.VotingPower
|
||||
}
|
||||
// else {
|
||||
// It's OK. We include stray signatures (~votes for nil) to measure
|
||||
// validator availability.
|
||||
// }
|
||||
}
|
||||
|
||||
if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed {
|
||||
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
|
||||
}
|
||||
|
||||
return nil
|
||||
return VerifyCommit(chainID, vals, blockID, height, commit)
|
||||
}
|
||||
|
||||
// LIGHT CLIENT VERIFICATION METHODS
|
||||
|
||||
// VerifyCommitLight verifies +2/3 of the set had signed the given commit.
|
||||
//
|
||||
// This method is primarily used by the light client and does not check all the
|
||||
// signatures.
|
||||
func (vals *ValidatorSet) VerifyCommitLight(chainID string, blockID BlockID,
|
||||
height int64, commit *Commit) error {
|
||||
|
||||
if vals.Size() != len(commit.Signatures) {
|
||||
return NewErrInvalidCommitSignatures(vals.Size(), len(commit.Signatures))
|
||||
}
|
||||
|
||||
// Validate Height and BlockID.
|
||||
if height != commit.Height {
|
||||
return NewErrInvalidCommitHeight(height, commit.Height)
|
||||
}
|
||||
if !blockID.Equals(commit.BlockID) {
|
||||
return fmt.Errorf("invalid commit -- wrong block ID: want %v, got %v",
|
||||
blockID, commit.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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
talliedVotingPower += val.VotingPower
|
||||
|
||||
// return as soon as +2/3 of the signatures are verified
|
||||
if talliedVotingPower > votingPowerNeeded {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded}
|
||||
return VerifyCommitLight(chainID, vals, blockID, height, commit)
|
||||
}
|
||||
|
||||
// VerifyCommitLightTrusting verifies that trustLevel of the validator set signed
|
||||
// this commit.
|
||||
//
|
||||
// NOTE the given validators do not necessarily correspond to the validator set
|
||||
// for this commit, but there may be some intersection.
|
||||
//
|
||||
// This method is primarily used by the light client and does not check all the
|
||||
// signatures.
|
||||
func (vals *ValidatorSet) VerifyCommitLightTrusting(chainID string, commit *Commit, trustLevel tmmath.Fraction) error {
|
||||
// sanity check
|
||||
if trustLevel.Denominator == 0 {
|
||||
return errors.New("trustLevel has zero Denominator")
|
||||
}
|
||||
|
||||
var (
|
||||
talliedVotingPower int64
|
||||
seenVals = make(map[int32]int, len(commit.Signatures)) // validator index -> commit index
|
||||
)
|
||||
|
||||
// Safely calculate voting power needed.
|
||||
totalVotingPowerMulByNumerator, overflow := safeMul(vals.TotalVotingPower(), int64(trustLevel.Numerator))
|
||||
if overflow {
|
||||
return errors.New("int64 overflow while calculating voting power needed. please provide smaller trustLevel numerator")
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded}
|
||||
return VerifyCommitLightTrusting(chainID, vals, commit, trustLevel)
|
||||
}
|
||||
|
||||
// findPreviousProposer reverses the compare proposer priority function to find the validator
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -665,155 +664,6 @@ func TestSafeSubClip(t *testing.T) {
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
// Check VerifyCommit, VerifyCommitLight and VerifyCommitLightTrusting basic
|
||||
// verification.
|
||||
func TestValidatorSet_VerifyCommit_All(t *testing.T) {
|
||||
var (
|
||||
privKey = ed25519.GenPrivKey()
|
||||
pubKey = privKey.PubKey()
|
||||
v1 = NewValidator(pubKey, 1000)
|
||||
vset = NewValidatorSet([]*Validator{v1})
|
||||
|
||||
chainID = "Lalande21185"
|
||||
)
|
||||
|
||||
vote := examplePrecommit()
|
||||
vote.ValidatorAddress = pubKey.Address()
|
||||
v := vote.ToProto()
|
||||
sig, err := privKey.Sign(VoteSignBytes(chainID, v))
|
||||
require.NoError(t, err)
|
||||
vote.Signature = sig
|
||||
|
||||
commit := NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{vote.CommitSig()})
|
||||
|
||||
vote2 := *vote
|
||||
sig2, err := privKey.Sign(VoteSignBytes("EpsilonEridani", v))
|
||||
require.NoError(t, err)
|
||||
vote2.Signature = sig2
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
chainID string
|
||||
blockID BlockID
|
||||
height int64
|
||||
commit *Commit
|
||||
expErr bool
|
||||
}{
|
||||
{"good", chainID, vote.BlockID, vote.Height, commit, false},
|
||||
|
||||
{"wrong signature (#0)", "EpsilonEridani", vote.BlockID, vote.Height, commit, true},
|
||||
{"wrong block ID", chainID, makeBlockIDRandom(), vote.Height, commit, true},
|
||||
{"wrong height", chainID, vote.BlockID, vote.Height - 1, commit, true},
|
||||
|
||||
{"wrong set size: 1 vs 0", chainID, vote.BlockID, vote.Height,
|
||||
NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{}), true},
|
||||
|
||||
{"wrong set size: 1 vs 2", chainID, vote.BlockID, vote.Height,
|
||||
NewCommit(vote.Height, vote.Round, vote.BlockID,
|
||||
[]CommitSig{vote.CommitSig(), {BlockIDFlag: BlockIDFlagAbsent}}), true},
|
||||
|
||||
{"insufficient voting power: got 0, needed more than 666", chainID, vote.BlockID, vote.Height,
|
||||
NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{{BlockIDFlag: BlockIDFlagAbsent}}), true},
|
||||
|
||||
{"wrong signature (#0)", chainID, vote.BlockID, vote.Height,
|
||||
NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{vote2.CommitSig()}), true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
err := vset.VerifyCommit(tc.chainID, tc.blockID, tc.height, tc.commit)
|
||||
if tc.expErr {
|
||||
if assert.Error(t, err, "VerifyCommit") {
|
||||
assert.Contains(t, err.Error(), tc.description, "VerifyCommit")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "VerifyCommit")
|
||||
}
|
||||
|
||||
err = vset.VerifyCommitLight(tc.chainID, tc.blockID, tc.height, tc.commit)
|
||||
if tc.expErr {
|
||||
if assert.Error(t, err, "VerifyCommitLight") {
|
||||
assert.Contains(t, err.Error(), tc.description, "VerifyCommitLight")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "VerifyCommitLight")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommit_CheckAllSignatures(t *testing.T) {
|
||||
var (
|
||||
chainID = "test_chain_id"
|
||||
h = int64(3)
|
||||
blockID = makeBlockIDRandom()
|
||||
)
|
||||
|
||||
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
|
||||
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
// malleate 4th signature
|
||||
vote := voteSet.GetByIndex(3)
|
||||
v := vote.ToProto()
|
||||
err = vals[3].SignVote("CentaurusA", v)
|
||||
require.NoError(t, err)
|
||||
vote.Signature = v.Signature
|
||||
commit.Signatures[3] = vote.CommitSig()
|
||||
|
||||
err = valSet.VerifyCommit(chainID, blockID, h, commit)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong signature (#3)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLight_ReturnsAsSoonAsMajorityOfVotingPowerSigned(t *testing.T) {
|
||||
var (
|
||||
chainID = "test_chain_id"
|
||||
h = int64(3)
|
||||
blockID = makeBlockIDRandom()
|
||||
)
|
||||
|
||||
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
|
||||
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
// malleate 4th signature (3 signatures are enough for 2/3+)
|
||||
vote := voteSet.GetByIndex(3)
|
||||
v := vote.ToProto()
|
||||
err = vals[3].SignVote("CentaurusA", v)
|
||||
require.NoError(t, err)
|
||||
vote.Signature = v.Signature
|
||||
commit.Signatures[3] = vote.CommitSig()
|
||||
|
||||
err = valSet.VerifyCommitLight(chainID, blockID, h, commit)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLightTrusting_ReturnsAsSoonAsTrustLevelOfVotingPowerSigned(t *testing.T) {
|
||||
var (
|
||||
chainID = "test_chain_id"
|
||||
h = int64(3)
|
||||
blockID = makeBlockIDRandom()
|
||||
)
|
||||
|
||||
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
|
||||
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
// malleate 3rd signature (2 signatures are enough for 1/3+ trust level)
|
||||
vote := voteSet.GetByIndex(2)
|
||||
v := vote.ToProto()
|
||||
err = vals[2].SignVote("CentaurusA", v)
|
||||
require.NoError(t, err)
|
||||
vote.Signature = v.Signature
|
||||
commit.Signatures[2] = vote.CommitSig()
|
||||
|
||||
err = valSet.VerifyCommitLightTrusting(chainID, commit, tmmath.Fraction{Numerator: 1, Denominator: 3})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmptySet(t *testing.T) {
|
||||
|
||||
var valList []*Validator
|
||||
@@ -1517,62 +1367,6 @@ func TestValSetUpdateOverflowRelated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLightTrusting(t *testing.T) {
|
||||
var (
|
||||
blockID = makeBlockIDRandom()
|
||||
voteSet, originalValset, vals = randVoteSet(1, 1, tmproto.PrecommitType, 6, 1)
|
||||
commit, err = MakeCommit(blockID, 1, 1, voteSet, vals, time.Now())
|
||||
newValSet, _ = RandValidatorSet(2, 1)
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
valSet *ValidatorSet
|
||||
err bool
|
||||
}{
|
||||
// good
|
||||
0: {
|
||||
valSet: originalValset,
|
||||
err: false,
|
||||
},
|
||||
// bad - no overlap between validator sets
|
||||
1: {
|
||||
valSet: newValSet,
|
||||
err: true,
|
||||
},
|
||||
// good - first two are different but the rest of the same -> >1/3
|
||||
2: {
|
||||
valSet: NewValidatorSet(append(newValSet.Validators, originalValset.Validators...)),
|
||||
err: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err = tc.valSet.VerifyCommitLightTrusting("test_chain_id", commit,
|
||||
tmmath.Fraction{Numerator: 1, Denominator: 3})
|
||||
if tc.err {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorSet_VerifyCommitLightTrustingErrorsOnOverflow(t *testing.T) {
|
||||
var (
|
||||
blockID = makeBlockIDRandom()
|
||||
voteSet, valSet, vals = randVoteSet(1, 1, tmproto.PrecommitType, 1, MaxTotalVotingPower)
|
||||
commit, err = MakeCommit(blockID, 1, 1, voteSet, vals, time.Now())
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = valSet.VerifyCommitLightTrusting("test_chain_id", commit,
|
||||
tmmath.Fraction{Numerator: 25, Denominator: 55})
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "int64 overflow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeMul(t *testing.T) {
|
||||
testCases := []struct {
|
||||
a int64
|
||||
|
||||
Reference in New Issue
Block a user