mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-03 18:42:14 +00:00
* Split vote verification/validation based on vote extensions Some parts of the code need vote extensions to be verified and validated (mostly in consensus), and other parts of the code don't because its possible that, in some cases (as per RFC 017), we won't have vote extensions. This explicitly facilitates that split. Signed-off-by: Thane Thomson <connect@thanethomson.com> * Only sign extensions in precommits, not prevotes Signed-off-by: Thane Thomson <connect@thanethomson.com> * Update privval/file.go Co-authored-by: M. J. Fromberger <fromberger@interchain.io> * Apply suggestions from code review Co-authored-by: M. J. Fromberger <fromberger@interchain.io> * Temporarily disable extension requirement again for E2E testing Signed-off-by: Thane Thomson <connect@thanethomson.com> * Reorganize comment for clarity Signed-off-by: Thane Thomson <connect@thanethomson.com> * Leave vote validation and pre-call nil check up to caller of VoteToProto Signed-off-by: Thane Thomson <connect@thanethomson.com> * Split complex vote validation test into multiple tests Signed-off-by: Thane Thomson <connect@thanethomson.com> * Universally enforce no vote extensions on any vote type but precommits Signed-off-by: Thane Thomson <connect@thanethomson.com> * Make error messages more generic Signed-off-by: Thane Thomson <connect@thanethomson.com> * Verify with vote extensions when constructing a VoteSet Signed-off-by: Thane Thomson <connect@thanethomson.com> * Expand comment for clarity Signed-off-by: Thane Thomson <connect@thanethomson.com> * Add extension check for prevotes prior to signing votes Signed-off-by: Thane Thomson <connect@thanethomson.com> * Fix supporting test code to only inject extensions into precommits Signed-off-by: Thane Thomson <connect@thanethomson.com> * Separate vote malleation from signing in vote tests for clarity Signed-off-by: Thane Thomson <connect@thanethomson.com> * Add extension signature length check and corresponding test Signed-off-by: Thane Thomson <connect@thanethomson.com> * Perform basic vote validation in CommitToVoteSet Signed-off-by: Thane Thomson <connect@thanethomson.com> Co-authored-by: M. J. Fromberger <fromberger@interchain.io>
349 lines
10 KiB
Go
349 lines
10 KiB
Go
package types
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/tendermint/tendermint/crypto"
|
|
"github.com/tendermint/tendermint/internal/libs/protoio"
|
|
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
|
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
|
)
|
|
|
|
const (
|
|
nilVoteStr string = "nil-Vote"
|
|
)
|
|
|
|
var (
|
|
ErrVoteUnexpectedStep = errors.New("unexpected step")
|
|
ErrVoteInvalidValidatorIndex = errors.New("invalid validator index")
|
|
ErrVoteInvalidValidatorAddress = errors.New("invalid validator address")
|
|
ErrVoteInvalidSignature = errors.New("invalid signature")
|
|
ErrVoteInvalidBlockHash = errors.New("invalid block hash")
|
|
ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature")
|
|
ErrVoteNil = errors.New("nil vote")
|
|
ErrVoteInvalidExtension = errors.New("invalid vote extension")
|
|
)
|
|
|
|
type ErrVoteConflictingVotes struct {
|
|
VoteA *Vote
|
|
VoteB *Vote
|
|
}
|
|
|
|
func (err *ErrVoteConflictingVotes) Error() string {
|
|
return fmt.Sprintf("conflicting votes from validator %X", err.VoteA.ValidatorAddress)
|
|
}
|
|
|
|
func NewConflictingVoteError(vote1, vote2 *Vote) *ErrVoteConflictingVotes {
|
|
return &ErrVoteConflictingVotes{
|
|
VoteA: vote1,
|
|
VoteB: vote2,
|
|
}
|
|
}
|
|
|
|
// Address is hex bytes.
|
|
type Address = crypto.Address
|
|
|
|
// Vote represents a prevote, precommit, or commit vote from validators for
|
|
// consensus.
|
|
type Vote struct {
|
|
Type tmproto.SignedMsgType `json:"type"`
|
|
Height int64 `json:"height,string"`
|
|
Round int32 `json:"round"` // assume there will not be greater than 2_147_483_647 rounds
|
|
BlockID BlockID `json:"block_id"` // zero if vote is nil.
|
|
Timestamp time.Time `json:"timestamp"`
|
|
ValidatorAddress Address `json:"validator_address"`
|
|
ValidatorIndex int32 `json:"validator_index"`
|
|
Signature []byte `json:"signature"`
|
|
Extension []byte `json:"extension"`
|
|
ExtensionSignature []byte `json:"extension_signature"`
|
|
}
|
|
|
|
// VoteFromProto attempts to convert the given serialization (Protobuf) type to
|
|
// our Vote domain type. No validation is performed on the resulting vote -
|
|
// this is left up to the caller to decide whether to call ValidateBasic or
|
|
// ValidateWithExtension.
|
|
func VoteFromProto(pv *tmproto.Vote) (*Vote, error) {
|
|
blockID, err := BlockIDFromProto(&pv.BlockID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Vote{
|
|
Type: pv.Type,
|
|
Height: pv.Height,
|
|
Round: pv.Round,
|
|
BlockID: *blockID,
|
|
Timestamp: pv.Timestamp,
|
|
ValidatorAddress: pv.ValidatorAddress,
|
|
ValidatorIndex: pv.ValidatorIndex,
|
|
Signature: pv.Signature,
|
|
Extension: pv.Extension,
|
|
ExtensionSignature: pv.ExtensionSignature,
|
|
}, nil
|
|
}
|
|
|
|
// CommitSig converts the Vote to a CommitSig.
|
|
func (vote *Vote) CommitSig() CommitSig {
|
|
if vote == nil {
|
|
return NewCommitSigAbsent()
|
|
}
|
|
|
|
var blockIDFlag BlockIDFlag
|
|
switch {
|
|
case vote.BlockID.IsComplete():
|
|
blockIDFlag = BlockIDFlagCommit
|
|
case vote.BlockID.IsNil():
|
|
blockIDFlag = BlockIDFlagNil
|
|
default:
|
|
panic(fmt.Sprintf("Invalid vote %v - expected BlockID to be either empty or complete", vote))
|
|
}
|
|
|
|
return CommitSig{
|
|
BlockIDFlag: blockIDFlag,
|
|
ValidatorAddress: vote.ValidatorAddress,
|
|
Timestamp: vote.Timestamp,
|
|
Signature: vote.Signature,
|
|
}
|
|
}
|
|
|
|
// VoteSignBytes returns the proto-encoding of the canonicalized Vote, for
|
|
// signing. Panics if the marshaling fails.
|
|
//
|
|
// The encoded Protobuf message is varint length-prefixed (using MarshalDelimited)
|
|
// for backwards-compatibility with the Amino encoding, due to e.g. hardware
|
|
// devices that rely on this encoding.
|
|
//
|
|
// See CanonicalizeVote
|
|
func VoteSignBytes(chainID string, vote *tmproto.Vote) []byte {
|
|
pb := CanonicalizeVote(chainID, vote)
|
|
bz, err := protoio.MarshalDelimited(&pb)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return bz
|
|
}
|
|
|
|
// VoteExtensionSignBytes returns the proto-encoding of the canonicalized vote
|
|
// extension for signing. Panics if the marshaling fails.
|
|
//
|
|
// Similar to VoteSignBytes, the encoded Protobuf message is varint
|
|
// length-prefixed for backwards-compatibility with the Amino encoding.
|
|
func VoteExtensionSignBytes(chainID string, vote *tmproto.Vote) []byte {
|
|
pb := CanonicalizeVoteExtension(chainID, vote)
|
|
bz, err := protoio.MarshalDelimited(&pb)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return bz
|
|
}
|
|
|
|
func (vote *Vote) Copy() *Vote {
|
|
voteCopy := *vote
|
|
return &voteCopy
|
|
}
|
|
|
|
// String returns a string representation of Vote.
|
|
//
|
|
// 1. validator index
|
|
// 2. first 6 bytes of validator address
|
|
// 3. height
|
|
// 4. round,
|
|
// 5. type byte
|
|
// 6. type string
|
|
// 7. first 6 bytes of block hash
|
|
// 8. first 6 bytes of signature
|
|
// 9. first 6 bytes of vote extension
|
|
// 10. timestamp
|
|
func (vote *Vote) String() string {
|
|
if vote == nil {
|
|
return nilVoteStr
|
|
}
|
|
|
|
var typeString string
|
|
switch vote.Type {
|
|
case tmproto.PrevoteType:
|
|
typeString = "Prevote"
|
|
case tmproto.PrecommitType:
|
|
typeString = "Precommit"
|
|
default:
|
|
panic("Unknown vote type")
|
|
}
|
|
|
|
return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %X %X @ %s}",
|
|
vote.ValidatorIndex,
|
|
tmbytes.Fingerprint(vote.ValidatorAddress),
|
|
vote.Height,
|
|
vote.Round,
|
|
vote.Type,
|
|
typeString,
|
|
tmbytes.Fingerprint(vote.BlockID.Hash),
|
|
tmbytes.Fingerprint(vote.Signature),
|
|
tmbytes.Fingerprint(vote.Extension),
|
|
CanonicalTime(vote.Timestamp),
|
|
)
|
|
}
|
|
|
|
func (vote *Vote) verifyAndReturnProto(chainID string, pubKey crypto.PubKey) (*tmproto.Vote, error) {
|
|
if !bytes.Equal(pubKey.Address(), vote.ValidatorAddress) {
|
|
return nil, ErrVoteInvalidValidatorAddress
|
|
}
|
|
v := vote.ToProto()
|
|
if !pubKey.VerifySignature(VoteSignBytes(chainID, v), vote.Signature) {
|
|
return nil, ErrVoteInvalidSignature
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// Verify checks whether the signature associated with this vote corresponds to
|
|
// the given chain ID and public key. This function does not validate vote
|
|
// extension signatures - to do so, use VerifyWithExtension instead.
|
|
func (vote *Vote) Verify(chainID string, pubKey crypto.PubKey) error {
|
|
_, err := vote.verifyAndReturnProto(chainID, pubKey)
|
|
return err
|
|
}
|
|
|
|
// VerifyWithExtension performs the same verification as Verify, but
|
|
// additionally checks whether the vote extension signature corresponds to the
|
|
// given chain ID and public key. We only verify vote extension signatures for
|
|
// precommits.
|
|
func (vote *Vote) VerifyWithExtension(chainID string, pubKey crypto.PubKey) error {
|
|
v, err := vote.verifyAndReturnProto(chainID, pubKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// We only verify vote extension signatures for precommits.
|
|
if vote.Type == tmproto.PrecommitType {
|
|
extSignBytes := VoteExtensionSignBytes(chainID, v)
|
|
// TODO: Remove extension signature nil check to enforce vote extension
|
|
// signing once we resolve https://github.com/tendermint/tendermint/issues/8272
|
|
if vote.ExtensionSignature != nil && !pubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
|
|
return ErrVoteInvalidSignature
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateBasic checks whether the vote is well-formed. It does not, however,
|
|
// check vote extensions - for vote validation with vote extension validation,
|
|
// use ValidateWithExtension.
|
|
func (vote *Vote) ValidateBasic() error {
|
|
if !IsVoteTypeValid(vote.Type) {
|
|
return errors.New("invalid Type")
|
|
}
|
|
|
|
if vote.Height < 0 {
|
|
return errors.New("negative Height")
|
|
}
|
|
|
|
if vote.Round < 0 {
|
|
return errors.New("negative Round")
|
|
}
|
|
|
|
// NOTE: Timestamp validation is subtle and handled elsewhere.
|
|
|
|
if err := vote.BlockID.ValidateBasic(); err != nil {
|
|
return fmt.Errorf("wrong BlockID: %w", err)
|
|
}
|
|
|
|
// BlockID.ValidateBasic would not err if we for instance have an empty hash but a
|
|
// non-empty PartsSetHeader:
|
|
if !vote.BlockID.IsNil() && !vote.BlockID.IsComplete() {
|
|
return fmt.Errorf("blockID must be either empty or complete, got: %v", vote.BlockID)
|
|
}
|
|
|
|
if len(vote.ValidatorAddress) != crypto.AddressSize {
|
|
return fmt.Errorf("expected ValidatorAddress size to be %d bytes, got %d bytes",
|
|
crypto.AddressSize,
|
|
len(vote.ValidatorAddress),
|
|
)
|
|
}
|
|
if vote.ValidatorIndex < 0 {
|
|
return errors.New("negative ValidatorIndex")
|
|
}
|
|
if len(vote.Signature) == 0 {
|
|
return errors.New("signature is missing")
|
|
}
|
|
|
|
if len(vote.Signature) > MaxSignatureSize {
|
|
return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize)
|
|
}
|
|
|
|
// We should only ever see vote extensions in precommits.
|
|
if vote.Type != tmproto.PrecommitType {
|
|
if len(vote.Extension) > 0 {
|
|
return errors.New("unexpected vote extension")
|
|
}
|
|
if len(vote.ExtensionSignature) > 0 {
|
|
return errors.New("unexpected vote extension signature")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateWithExtension performs the same validations as ValidateBasic, but
|
|
// additionally checks whether a vote extension signature is present. This
|
|
// function is used in places where vote extension signatures are expected.
|
|
func (vote *Vote) ValidateWithExtension() error {
|
|
if err := vote.ValidateBasic(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// We should always see vote extension signatures in precommits
|
|
if vote.Type == tmproto.PrecommitType {
|
|
// TODO(thane): Remove extension length check once
|
|
// https://github.com/tendermint/tendermint/issues/8272 is
|
|
// resolved.
|
|
if len(vote.Extension) > 0 && len(vote.ExtensionSignature) == 0 {
|
|
return errors.New("vote extension signature is missing")
|
|
}
|
|
if len(vote.ExtensionSignature) > MaxSignatureSize {
|
|
return fmt.Errorf("vote extension signature is too big (max: %d)", MaxSignatureSize)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ToProto converts the handwritten type to proto generated type
|
|
// return type, nil if everything converts safely, otherwise nil, error
|
|
func (vote *Vote) ToProto() *tmproto.Vote {
|
|
if vote == nil {
|
|
return nil
|
|
}
|
|
|
|
return &tmproto.Vote{
|
|
Type: vote.Type,
|
|
Height: vote.Height,
|
|
Round: vote.Round,
|
|
BlockID: vote.BlockID.ToProto(),
|
|
Timestamp: vote.Timestamp,
|
|
ValidatorAddress: vote.ValidatorAddress,
|
|
ValidatorIndex: vote.ValidatorIndex,
|
|
Signature: vote.Signature,
|
|
Extension: vote.Extension,
|
|
ExtensionSignature: vote.ExtensionSignature,
|
|
}
|
|
}
|
|
|
|
func VotesToProto(votes []*Vote) []*tmproto.Vote {
|
|
if votes == nil {
|
|
return nil
|
|
}
|
|
|
|
res := make([]*tmproto.Vote, 0, len(votes))
|
|
for _, vote := range votes {
|
|
v := vote.ToProto()
|
|
// protobuf crashes when serializing "repeated" fields with nil elements
|
|
if v != nil {
|
|
res = append(res, v)
|
|
}
|
|
}
|
|
return res
|
|
}
|