diff --git a/consensus/common_test.go b/consensus/common_test.go index d5bc92c62..ef65bc441 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -132,7 +132,12 @@ func (vs *validatorStub) signVote( // Sign vote for type/hash/header func signVote(vs *validatorStub, voteType tmproto.SignedMsgType, hash []byte, header types.PartSetHeader) *types.Vote { - v, err := vs.signVote(voteType, hash, header, []byte("extension")) + var ext []byte + if voteType == tmproto.PrecommitType { + ext = []byte("extension") + } + v, err := vs.signVote(voteType, hash, header, ext) + if err != nil { panic(fmt.Errorf("failed to sign vote: %v", err)) } @@ -489,7 +494,7 @@ func ensureNoNewEvent(ch <-chan tmpubsub.Message, timeout time.Duration, func ensureNoNewEventOnChannel(ch <-chan tmpubsub.Message) { ensureNoNewEvent( ch, - ensureTimeout, + ensureTimeout*8/10, // 20% leniency for goroutine scheduling uncertainty "We should be stuck waiting, not receiving new event on the channel") } @@ -720,7 +725,7 @@ func ensurePrecommitTimeout(ch <-chan tmpubsub.Message) { func ensureNewEventOnChannel(ch <-chan tmpubsub.Message) { select { - case <-time.After(ensureTimeout): + case <-time.After(ensureTimeout * 12 / 10): // 20% leniency for goroutine scheduling uncertainty panic("Timeout expired while waiting for new activity on the channel") case <-ch: } diff --git a/consensus/msgs.go b/consensus/msgs.go index 4631e1e53..b2c74cb0c 100644 --- a/consensus/msgs.go +++ b/consensus/msgs.go @@ -182,6 +182,8 @@ func MsgFromProto(p proto.Message) (Message, error) { Part: parts, } case *tmcons.Vote: + // Vote validation will be handled in the vote message ValidateBasic + // call below. vote, err := types.VoteFromProto(msg.Vote) if err != nil { return nil, fmt.Errorf("vote msg to proto error: %w", err) diff --git a/consensus/reactor.go b/consensus/reactor.go index 6a6d95f38..27ac4a045 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -1683,9 +1683,13 @@ type VoteMessage struct { Vote *types.Vote } -// ValidateBasic performs basic validation. +// ValidateBasic checks whether the vote within the message is well-formed. func (m *VoteMessage) ValidateBasic() error { - return m.Vote.ValidateBasic() + // Here we validate votes with vote extensions, since we require vote + // extensions to be sent in precommit messages during consensus. Prevote + // messages should never have vote extensions, and this is also validated + // here. + return m.Vote.ValidateWithExtension() } // String returns a string representation. diff --git a/privval/file.go b/privval/file.go index 31fa234a3..06e3176e1 100644 --- a/privval/file.go +++ b/privval/file.go @@ -315,11 +315,20 @@ func (pv *FilePV) signVote(chainID string, vote *tmproto.Vote) error { } signBytes := types.VoteSignBytes(chainID, vote) - extSignBytes := types.VoteExtensionSignBytes(chainID, vote) - // We always sign the vote extension. See below for details. - extSig, err := pv.Key.PrivKey.Sign(extSignBytes) - if err != nil { - return err + + // Vote extensions are non-deterministic, so it is possible that an + // application may have created a different extension. We therefore always + // re-sign the vote extensions of precommits. For prevotes, the extension + // signature will always be empty. + var extSig []byte + if vote.Type == tmproto.PrecommitType { + extSignBytes := types.VoteExtensionSignBytes(chainID, vote) + extSig, err = pv.Key.PrivKey.Sign(extSignBytes) + if err != nil { + return err + } + } else if len(vote.Extension) > 0 { + return errors.New("unexpected vote extension - extensions are only allowed in precommits") } // We might crash before writing to the wal, @@ -339,9 +348,6 @@ func (pv *FilePV) signVote(chainID string, vote *tmproto.Vote) error { err = fmt.Errorf("conflicting data") } - // Vote extensions are non-deterministic, so it's possible that an - // application may have created a different extension. We therefore - // always re-sign the vote extension. vote.ExtensionSignature = extSig return err diff --git a/types/block.go b/types/block.go index 3677be8af..acafe9599 100644 --- a/types/block.go +++ b/types/block.go @@ -771,7 +771,11 @@ func CommitToVoteSet(chainID string, commit *Commit, vals *ValidatorSet) *VoteSe if commitSig.Absent() { continue // OK, some precommits can be missing. } - added, err := voteSet.AddVote(commit.GetVote(int32(idx))) + vote := commit.GetVote(int32(idx)) + if err := vote.ValidateBasic(); err != nil { + panic(fmt.Errorf("failed to validate vote reconstructed from LastCommit: %w", err)) + } + added, err := voteSet.AddVote(vote) if !added || err != nil { panic(fmt.Sprintf("Failed to reconstruct LastCommit: %v", err)) } diff --git a/types/evidence.go b/types/evidence.go index df158de33..ba28060bf 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -164,14 +164,28 @@ func DuplicateVoteEvidenceFromProto(pb *tmproto.DuplicateVoteEvidence) (*Duplica return nil, errors.New("nil duplicate vote evidence") } - vA, err := VoteFromProto(pb.VoteA) - if err != nil { - return nil, err + var vA *Vote + if pb.VoteA != nil { + var err error + vA, err = VoteFromProto(pb.VoteA) + if err != nil { + return nil, err + } + if err = vA.ValidateBasic(); err != nil { + return nil, err + } } - vB, err := VoteFromProto(pb.VoteB) - if err != nil { - return nil, err + var vB *Vote + if pb.VoteB != nil { + var err error + vB, err = VoteFromProto(pb.VoteB) + if err != nil { + return nil, err + } + if err = vB.ValidateBasic(); err != nil { + return nil, err + } } dve := &DuplicateVoteEvidence{ diff --git a/types/priv_validator.go b/types/priv_validator.go index e947dd27c..7e878d797 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -83,9 +83,16 @@ func (pv MockPV) SignVote(chainID string, vote *tmproto.Vote) error { return err } vote.Signature = sig - extSig, err := pv.PrivKey.Sign(extSignBytes) - if err != nil { - return err + + var extSig []byte + // We only sign vote extensions for precommits + if vote.Type == tmproto.PrecommitType { + extSig, err = pv.PrivKey.Sign(extSignBytes) + if err != nil { + return err + } + } else if len(vote.Extension) > 0 { + return errors.New("unexpected vote extension - vote extensions are only allowed in precommits") } vote.ExtensionSignature = extSig return nil diff --git a/types/vote.go b/types/vote.go index 37eccbed0..d15dbf872 100644 --- a/types/vote.go +++ b/types/vote.go @@ -61,6 +61,30 @@ type Vote struct { 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 { @@ -164,24 +188,49 @@ func (vote *Vote) String() string { ) } -func (vote *Vote) Verify(chainID string, pubKey crypto.PubKey) error { +func (vote *Vote) verifyAndReturnProto(chainID string, pubKey crypto.PubKey) (*tmproto.Vote, error) { if !bytes.Equal(pubKey.Address(), vote.ValidatorAddress) { - return ErrVoteInvalidValidatorAddress + return nil, ErrVoteInvalidValidatorAddress } v := vote.ToProto() if !pubKey.VerifySignature(VoteSignBytes(chainID, v), vote.Signature) { - return ErrVoteInvalidSignature + return nil, ErrVoteInvalidSignature } - 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 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 performs basic validation. +// 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") @@ -224,10 +273,38 @@ func (vote *Vote) ValidateBasic() error { return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize) } - // TODO: Remove the extension length check such that we always require - // extension signatures to be present. - if len(vote.Extension) > 0 && len(vote.ExtensionSignature) == 0 { - return errors.New("vote extension signature is missing") + // 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 @@ -269,30 +346,3 @@ func VotesToProto(votes []*Vote) []*tmproto.Vote { } return res } - -// FromProto converts a proto generetad type to a handwritten type -// return type, nil if everything converts safely, otherwise nil, error -func VoteFromProto(pv *tmproto.Vote) (*Vote, error) { - if pv == nil { - return nil, errors.New("nil vote") - } - - blockID, err := BlockIDFromProto(&pv.BlockID) - if err != nil { - return nil, err - } - - vote := new(Vote) - vote.Type = pv.Type - vote.Height = pv.Height - vote.Round = pv.Round - vote.BlockID = *blockID - vote.Timestamp = pv.Timestamp - vote.ValidatorAddress = pv.ValidatorAddress - vote.ValidatorIndex = pv.ValidatorIndex - vote.Signature = pv.Signature - vote.Extension = pv.Extension - vote.ExtensionSignature = pv.ExtensionSignature - - return vote, vote.ValidateBasic() -} diff --git a/types/vote_set.go b/types/vote_set.go index 2fec82348..738e360ca 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -202,7 +202,7 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { } // Check signature. - if err := vote.Verify(voteSet.chainID, val.PubKey); err != nil { + if err := vote.VerifyWithExtension(voteSet.chainID, val.PubKey); err != nil { return false, fmt.Errorf("failed to verify vote with ChainID %s and PubKey %s: %w", voteSet.chainID, val.PubKey, err) } diff --git a/types/vote_test.go b/types/vote_test.go index 2cdd61de9..493019dac 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -21,7 +21,9 @@ func examplePrevote() *Vote { } func examplePrecommit() *Vote { - return exampleVote(byte(tmproto.PrecommitType)) + vote := exampleVote(byte(tmproto.PrecommitType)) + vote.ExtensionSignature = []byte("signature") + return vote } func exampleVote(t byte) *Vote { @@ -214,8 +216,8 @@ func TestVoteExtension(t *testing.T) { includeSignature: true, expectError: false, }, - // TODO: Re-enable once - // https://github.com/tendermint/tendermint/issues/8272 is resolved. + // TODO(thane): Re-enable once + // https://github.com/tendermint/tendermint/issues/8272 is resolved //{ // name: "no extension signature", // extension: []byte("extension"), @@ -262,7 +264,7 @@ func TestVoteExtension(t *testing.T) { if tc.includeSignature { vote.ExtensionSignature = v.ExtensionSignature } - err = vote.Verify("test_chain_id", pk) + err = vote.VerifyWithExtension("test_chain_id", pk) if tc.expectError { require.Error(t, err) } else { @@ -326,36 +328,107 @@ func TestVoteString(t *testing.T) { } } -func TestVoteValidateBasic(t *testing.T) { +func signVote(t *testing.T, pv PrivValidator, chainID string, vote *Vote) { + t.Helper() + + v := vote.ToProto() + require.NoError(t, pv.SignVote(chainID, v)) + vote.Signature = v.Signature + vote.ExtensionSignature = v.ExtensionSignature +} + +func TestValidVotes(t *testing.T) { privVal := NewMockPV() testCases := []struct { - testName string + name string + vote *Vote malleateVote func(*Vote) - expectErr bool }{ - {"Good Vote", func(v *Vote) {}, false}, - {"Negative Height", func(v *Vote) { v.Height = -1 }, true}, - {"Negative Round", func(v *Vote) { v.Round = -1 }, true}, - {"Invalid BlockID", func(v *Vote) { - v.BlockID = BlockID{[]byte{1, 2, 3}, PartSetHeader{111, []byte("blockparts")}} - }, true}, - {"Invalid Address", func(v *Vote) { v.ValidatorAddress = make([]byte, 1) }, true}, - {"Invalid ValidatorIndex", func(v *Vote) { v.ValidatorIndex = -1 }, true}, - {"Invalid Signature", func(v *Vote) { v.Signature = nil }, true}, - {"Too big Signature", func(v *Vote) { v.Signature = make([]byte, MaxSignatureSize+1) }, true}, + {"good prevote", examplePrevote(), func(v *Vote) {}}, + {"good precommit without vote extension", examplePrecommit(), func(v *Vote) { v.Extension = nil }}, + {"good precommit with vote extension", examplePrecommit(), func(v *Vote) { v.Extension = []byte("extension") }}, } for _, tc := range testCases { - tc := tc - t.Run(tc.testName, func(t *testing.T) { - vote := examplePrecommit() - v := vote.ToProto() - err := privVal.SignVote("test_chain_id", v) - vote.Signature = v.Signature - require.NoError(t, err) - tc.malleateVote(vote) - assert.Equal(t, tc.expectErr, vote.ValidateBasic() != nil, "Validate Basic had an unexpected result") - }) + signVote(t, privVal, "test_chain_id", tc.vote) + tc.malleateVote(tc.vote) + require.NoError(t, tc.vote.ValidateBasic(), "ValidateBasic for %s", tc.name) + require.NoError(t, tc.vote.ValidateWithExtension(), "ValidateWithExtension for %s", tc.name) + } +} + +func TestInvalidVotes(t *testing.T) { + privVal := NewMockPV() + + testCases := []struct { + name string + malleateVote func(*Vote) + }{ + {"negative height", func(v *Vote) { v.Height = -1 }}, + {"negative round", func(v *Vote) { v.Round = -1 }}, + {"invalid block ID", func(v *Vote) { v.BlockID = BlockID{[]byte{1, 2, 3}, PartSetHeader{111, []byte("blockparts")}} }}, + {"invalid address", func(v *Vote) { v.ValidatorAddress = make([]byte, 1) }}, + {"invalid validator index", func(v *Vote) { v.ValidatorIndex = -1 }}, + {"invalid signature", func(v *Vote) { v.Signature = nil }}, + {"oversized signature", func(v *Vote) { v.Signature = make([]byte, MaxSignatureSize+1) }}, + } + for _, tc := range testCases { + prevote := examplePrevote() + signVote(t, privVal, "test_chain_id", prevote) + tc.malleateVote(prevote) + require.Error(t, prevote.ValidateBasic(), "ValidateBasic for %s in invalid prevote", tc.name) + require.Error(t, prevote.ValidateWithExtension(), "ValidateWithExtension for %s in invalid prevote", tc.name) + + precommit := examplePrecommit() + signVote(t, privVal, "test_chain_id", precommit) + tc.malleateVote(precommit) + require.Error(t, precommit.ValidateBasic(), "ValidateBasic for %s in invalid precommit", tc.name) + require.Error(t, precommit.ValidateWithExtension(), "ValidateWithExtension for %s in invalid precommit", tc.name) + } +} + +func TestInvalidPrevotes(t *testing.T) { + privVal := NewMockPV() + + testCases := []struct { + name string + malleateVote func(*Vote) + }{ + {"vote extension present", func(v *Vote) { v.Extension = []byte("extension") }}, + {"vote extension signature present", func(v *Vote) { v.ExtensionSignature = []byte("signature") }}, + } + for _, tc := range testCases { + prevote := examplePrevote() + signVote(t, privVal, "test_chain_id", prevote) + tc.malleateVote(prevote) + require.Error(t, prevote.ValidateBasic(), "ValidateBasic for %s", tc.name) + require.Error(t, prevote.ValidateWithExtension(), "ValidateWithExtension for %s", tc.name) + } +} + +func TestInvalidPrecommitExtensions(t *testing.T) { + privVal := NewMockPV() + + testCases := []struct { + name string + malleateVote func(*Vote) + }{ + {"vote extension present without signature", func(v *Vote) { + v.Extension = []byte("extension") + v.ExtensionSignature = nil + }}, + // TODO(thane): Re-enable once https://github.com/tendermint/tendermint/issues/8272 is resolved + //{"missing vote extension signature", func(v *Vote) { v.ExtensionSignature = nil }}, + {"oversized vote extension signature", func(v *Vote) { v.ExtensionSignature = make([]byte, MaxSignatureSize+1) }}, + } + for _, tc := range testCases { + precommit := examplePrecommit() + signVote(t, privVal, "test_chain_id", precommit) + tc.malleateVote(precommit) + // We don't expect an error from ValidateBasic, because it doesn't + // handle vote extensions. + require.NoError(t, precommit.ValidateBasic(), "ValidateBasic for %s", tc.name) + require.Error(t, precommit.ValidateWithExtension(), "ValidateWithExtension for %s", tc.name) } } @@ -368,21 +441,28 @@ func TestVoteProtobuf(t *testing.T) { require.NoError(t, err) testCases := []struct { - msg string - v1 *Vote - expPass bool + msg string + vote *Vote + convertsOk bool + passesValidateBasic bool }{ - {"success", vote, true}, - {"fail vote validate basic", &Vote{}, false}, - {"failure nil", nil, false}, + {"success", vote, true, true}, + {"fail vote validate basic", &Vote{}, true, false}, } for _, tc := range testCases { - protoProposal := tc.v1.ToProto() + protoProposal := tc.vote.ToProto() v, err := VoteFromProto(protoProposal) - if tc.expPass { + if tc.convertsOk { require.NoError(t, err) - require.Equal(t, tc.v1, v, tc.msg) + } else { + require.Error(t, err) + } + + err = v.ValidateBasic() + if tc.passesValidateBasic { + require.NoError(t, err) + require.Equal(t, tc.vote, v, tc.msg) } else { require.Error(t, err) }