[cherry-picked] abci++: add consensus parameter logic to control vote extension require height (#8547)

This PR makes vote extensions optional within Tendermint. A new ConsensusParams field, called ABCIParams.VoteExtensionsEnableHeight, has been added to toggle whether or not extensions should be enabled or disabled depending on the current height of the consensus engine. Related to: #8453
This commit is contained in:
William Banfield
2022-11-30 21:19:16 +01:00
committed by Sergio Mena
parent 81bcf50690
commit 288ba2e76a
32 changed files with 1374 additions and 329 deletions
+96 -13
View File
@@ -749,22 +749,23 @@ func (ecs ExtendedCommitSig) ValidateBasic() error {
if len(ecs.Extension) > MaxVoteExtensionSize {
return fmt.Errorf("vote extension is too big (max: %d)", MaxVoteExtensionSize)
}
if len(ecs.ExtensionSignature) == 0 {
return errors.New("vote extension signature is missing")
}
if len(ecs.ExtensionSignature) > MaxSignatureSize {
return fmt.Errorf("vote extension signature is too big (max: %d)", MaxSignatureSize)
}
return nil
}
// We expect there to not be any vote extension or vote extension signature
// on nil or absent votes.
if len(ecs.Extension) != 0 {
return fmt.Errorf("vote extension is present for commit sig with block ID flag %v", ecs.BlockIDFlag)
if len(ecs.ExtensionSignature) == 0 && len(ecs.Extension) != 0 {
return errors.New("vote extension signature absent on vote with extension")
}
if len(ecs.ExtensionSignature) != 0 {
return fmt.Errorf("vote extension signature is present for commit sig with block ID flag %v", ecs.BlockIDFlag)
return nil
}
// EnsureExtensions validates that a vote extensions signature is present for
// this ExtendedCommitSig.
func (ecs ExtendedCommitSig) EnsureExtension() error {
if ecs.BlockIDFlag == BlockIDFlagCommit && len(ecs.ExtensionSignature) == 0 {
return errors.New("vote extension data is missing")
}
return nil
}
@@ -908,6 +909,26 @@ func (commit *Commit) Hash() tmbytes.HexBytes {
return commit.hash
}
// WrappedExtendedCommit wraps a commit as an ExtendedCommit.
// The VoteExtension fields of the resulting value will by nil.
// Wrapping a Commit as an ExtendedCommit is useful when an API
// requires an ExtendedCommit wire type but does not
// need the VoteExtension data.
func (commit *Commit) WrappedExtendedCommit() *ExtendedCommit {
cs := make([]ExtendedCommitSig, len(commit.Signatures))
for idx, s := range commit.Signatures {
cs[idx] = ExtendedCommitSig{
CommitSig: s,
}
}
return &ExtendedCommit{
Height: commit.Height,
Round: commit.Round,
BlockID: commit.BlockID,
ExtendedSignatures: cs,
}
}
// StringIndented returns a string representation of the commit.
func (commit *Commit) StringIndented(indent string) string {
if commit == nil {
@@ -1005,17 +1026,33 @@ func (ec *ExtendedCommit) Clone() *ExtendedCommit {
return &ecc
}
// ToExtendedVoteSet constructs a VoteSet from the Commit and validator set.
// Panics if signatures from the ExtendedCommit can't be added to the voteset.
// Panics if any of the votes have invalid or absent vote extension data.
// Inverse of VoteSet.MakeExtendedCommit().
func (ec *ExtendedCommit) ToExtendedVoteSet(chainID string, vals *ValidatorSet) *VoteSet {
voteSet := NewExtendedVoteSet(chainID, ec.Height, ec.Round, tmproto.PrecommitType, vals)
ec.addSigsToVoteSet(voteSet)
return voteSet
}
// ToVoteSet constructs a VoteSet from the Commit and validator set.
// Panics if signatures from the commit can't be added to the voteset.
// Panics if signatures from the ExtendedCommit can't be added to the voteset.
// Inverse of VoteSet.MakeExtendedCommit().
func (ec *ExtendedCommit) ToVoteSet(chainID string, vals *ValidatorSet) *VoteSet {
voteSet := NewVoteSet(chainID, ec.Height, ec.Round, tmproto.PrecommitType, vals)
ec.addSigsToVoteSet(voteSet)
return voteSet
}
// addSigsToVoteSet adds all of the signature to voteSet.
func (ec *ExtendedCommit) addSigsToVoteSet(voteSet *VoteSet) {
for idx, ecs := range ec.ExtendedSignatures {
if ecs.BlockIDFlag == BlockIDFlagAbsent {
continue // OK, some precommits can be missing.
}
vote := ec.GetExtendedVote(int32(idx))
if err := vote.ValidateWithExtension(); err != nil {
if err := vote.ValidateBasic(); err != nil {
panic(fmt.Errorf("failed to validate vote reconstructed from LastCommit: %w", err))
}
added, err := voteSet.AddVote(vote)
@@ -1023,12 +1060,58 @@ func (ec *ExtendedCommit) ToVoteSet(chainID string, vals *ValidatorSet) *VoteSet
panic(fmt.Errorf("failed to reconstruct vote set from extended commit: %w", err))
}
}
}
// ToVoteSet constructs a VoteSet from the Commit and validator set.
// Panics if signatures from the commit can't be added to the voteset.
// Inverse of VoteSet.MakeCommit().
func (commit *Commit) ToVoteSet(chainID string, vals *ValidatorSet) *VoteSet {
voteSet := NewVoteSet(chainID, commit.Height, commit.Round, tmproto.PrecommitType, vals)
for idx, cs := range commit.Signatures {
if cs.BlockIDFlag == BlockIDFlagAbsent {
continue // OK, some precommits can be missing.
}
vote := commit.GetVote(int32(idx))
if err := vote.ValidateBasic(); err != nil {
panic(fmt.Errorf("failed to validate vote reconstructed from commit: %w", err))
}
added, err := voteSet.AddVote(vote)
if !added || err != nil {
panic(fmt.Errorf("failed to reconstruct vote set from commit: %w", err))
}
}
return voteSet
}
// StripExtensions converts an ExtendedCommit to a Commit by removing all vote
// EnsureExtensions validates that a vote extensions signature is present for
// every ExtendedCommitSig in the ExtendedCommit.
func (ec *ExtendedCommit) EnsureExtensions() error {
for _, ecs := range ec.ExtendedSignatures {
if err := ecs.EnsureExtension(); err != nil {
return err
}
}
return nil
}
// StripExtensions removes all VoteExtension data from an ExtendedCommit. This
// is useful when dealing with an ExendedCommit but vote extension data is
// expected to be absent.
func (ec *ExtendedCommit) StripExtensions() bool {
stripped := false
for idx := range ec.ExtendedSignatures {
if len(ec.ExtendedSignatures[idx].Extension) > 0 || len(ec.ExtendedSignatures[idx].ExtensionSignature) > 0 {
stripped = true
}
ec.ExtendedSignatures[idx].Extension = nil
ec.ExtendedSignatures[idx].ExtensionSignature = nil
}
return stripped
}
// ToCommit converts an ExtendedCommit to a Commit by removing all vote
// extension-related fields.
func (ec *ExtendedCommit) StripExtensions() *Commit {
func (ec *ExtendedCommit) ToCommit() *Commit {
cs := make([]CommitSig, len(ec.ExtendedSignatures))
for idx, ecs := range ec.ExtendedSignatures {
cs[idx] = ecs.CommitSig
+131 -25
View File
@@ -3,6 +3,7 @@ package types
import (
// it is ok to use math/rand here: we do not need a cryptographically secure random
// number generator here and we can run the tests a bit faster
"context"
"crypto/rand"
"encoding/hex"
"math"
@@ -45,7 +46,7 @@ func TestBlockAddEvidence(t *testing.T) {
require.NoError(t, err)
evList := []Evidence{ev}
block := MakeBlock(h, txs, extCommit.StripExtensions(), evList)
block := MakeBlock(h, txs, extCommit.ToCommit(), evList)
require.NotNil(t, block)
require.Equal(t, 1, len(block.Evidence.Evidence))
require.NotNil(t, block.EvidenceHash)
@@ -61,7 +62,7 @@ func TestBlockValidateBasic(t *testing.T) {
voteSet, valSet, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
extCommit, err := MakeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
ev, err := NewMockDuplicateVoteEvidenceWithValidator(h, time.Now(), vals[0], "block-test-chain")
require.NoError(t, err)
@@ -139,7 +140,7 @@ func TestBlockMakePartSetWithEvidence(t *testing.T) {
require.NoError(t, err)
evList := []Evidence{ev}
partSet, err := MakeBlock(h, []Tx{Tx("Hello World")}, extCommit.StripExtensions(), evList).MakePartSet(512)
partSet, err := MakeBlock(h, []Tx{Tx("Hello World")}, extCommit.ToCommit(), evList).MakePartSet(512)
require.NoError(t, err)
assert.NotNil(t, partSet)
@@ -159,7 +160,7 @@ func TestBlockHashesTo(t *testing.T) {
require.NoError(t, err)
evList := []Evidence{ev}
block := MakeBlock(h, []Tx{Tx("Hello World")}, extCommit.StripExtensions(), evList)
block := MakeBlock(h, []Tx{Tx("Hello World")}, extCommit.ToCommit(), evList)
block.ValidatorsHash = valSet.Hash()
assert.False(t, block.HashesTo([]byte{}))
assert.False(t, block.HashesTo([]byte("something else")))
@@ -443,7 +444,7 @@ func randCommit(now time.Time) *Commit {
if err != nil {
panic(err)
}
return commit.StripExtensions()
return commit.ToCommit()
}
func hexBytesFromString(s string) bytes.HexBytes {
@@ -515,30 +516,135 @@ func TestBlockMaxDataBytesNoEvidence(t *testing.T) {
}
}
// TestVoteSetToExtendedCommit tests that the extended commit produced from a
// vote set contains the same vote information as the vote set. The test ensures
// that the MakeExtendedCommit method behaves as expected, whether vote extensions
// are present in the original votes or not.
func TestVoteSetToExtendedCommit(t *testing.T) {
for _, testCase := range []struct {
name string
includeExtension bool
}{
{
name: "no extensions",
includeExtension: false,
},
{
name: "with extensions",
includeExtension: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
blockID := makeBlockIDRandom()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
valSet, vals := randValidatorPrivValSet(ctx, t, 10, 1)
var voteSet *VoteSet
if testCase.includeExtension {
voteSet = NewExtendedVoteSet("test_chain_id", 3, 1, tmproto.PrecommitType, valSet)
} else {
voteSet = NewVoteSet("test_chain_id", 3, 1, tmproto.PrecommitType, valSet)
}
for i := 0; i < len(vals); i++ {
pubKey, err := vals[i].GetPubKey(ctx)
require.NoError(t, err)
vote := &Vote{
ValidatorAddress: pubKey.Address(),
ValidatorIndex: int32(i),
Height: 3,
Round: 1,
Type: tmproto.PrecommitType,
BlockID: blockID,
Timestamp: time.Now(),
}
v := vote.ToProto()
err = vals[i].SignVote(ctx, voteSet.ChainID(), v)
require.NoError(t, err)
vote.Signature = v.Signature
if testCase.includeExtension {
vote.ExtensionSignature = v.ExtensionSignature
}
added, err := voteSet.AddVote(vote)
require.NoError(t, err)
require.True(t, added)
}
ec := voteSet.MakeExtendedCommit()
for i := int32(0); int(i) < len(vals); i++ {
vote1 := voteSet.GetByIndex(i)
vote2 := ec.GetExtendedVote(i)
vote1bz, err := vote1.ToProto().Marshal()
require.NoError(t, err)
vote2bz, err := vote2.ToProto().Marshal()
require.NoError(t, err)
assert.Equal(t, vote1bz, vote2bz)
}
})
}
}
// TestExtendedCommitToVoteSet tests that the vote set produced from an extended commit
// contains the same vote information as the extended commit. The test ensures
// that the ToVoteSet method behaves as expected, whether vote extensions
// are present in the original votes or not.
func TestExtendedCommitToVoteSet(t *testing.T) {
lastID := makeBlockIDRandom()
h := int64(3)
for _, testCase := range []struct {
name string
includeExtension bool
}{
{
name: "no extensions",
includeExtension: false,
},
{
name: "with extensions",
includeExtension: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
lastID := makeBlockIDRandom()
h := int64(3)
voteSet, valSet, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
extCommit, err := MakeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
assert.NoError(t, err)
voteSet, valSet, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
extCommit, err := MakeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
assert.NoError(t, err)
chainID := voteSet.ChainID()
voteSet2 := extCommit.ToVoteSet(chainID, valSet)
if !testCase.includeExtension {
for i := 0; i < len(vals); i++ {
v := voteSet.GetByIndex(int32(i))
v.Extension = nil
v.ExtensionSignature = nil
extCommit.ExtendedSignatures[i].Extension = nil
extCommit.ExtendedSignatures[i].ExtensionSignature = nil
}
}
for i := int32(0); int(i) < len(vals); i++ {
vote1 := voteSet.GetByIndex(i)
vote2 := voteSet2.GetByIndex(i)
vote3 := extCommit.GetExtendedVote(i)
chainID := voteSet.ChainID()
var voteSet2 *VoteSet
if testCase.includeExtension {
voteSet2 = extCommit.ToExtendedVoteSet(chainID, valSet)
} else {
voteSet2 = extCommit.ToVoteSet(chainID, valSet)
}
vote1bz, err := vote1.ToProto().Marshal()
require.NoError(t, err)
vote2bz, err := vote2.ToProto().Marshal()
require.NoError(t, err)
vote3bz, err := vote3.ToProto().Marshal()
require.NoError(t, err)
assert.Equal(t, vote1bz, vote2bz)
assert.Equal(t, vote1bz, vote3bz)
for i := int32(0); int(i) < len(vals); i++ {
vote1 := voteSet.GetByIndex(i)
vote2 := voteSet2.GetByIndex(i)
vote3 := extCommit.GetExtendedVote(i)
vote1bz, err := vote1.ToProto().Marshal()
require.NoError(t, err)
vote2bz, err := vote2.ToProto().Marshal()
require.NoError(t, err)
vote3bz, err := vote3.ToProto().Marshal()
require.NoError(t, err)
assert.Equal(t, vote1bz, vote2bz)
assert.Equal(t, vote1bz, vote3bz)
}
})
}
}
@@ -590,7 +696,7 @@ func TestCommitToVoteSetWithVotesForNilBlock(t *testing.T) {
if tc.valid {
extCommit := voteSet.MakeExtendedCommit() // panics without > 2/3 valid votes
assert.NotNil(t, extCommit)
err := valSet.VerifyCommit(voteSet.ChainID(), blockID, height-1, extCommit.StripExtensions())
err := valSet.VerifyCommit(voteSet.ChainID(), blockID, height-1, extCommit.ToCommit())
assert.Nil(t, err)
} else {
assert.Panics(t, func() { voteSet.MakeExtendedCommit() })
+2 -2
View File
@@ -101,7 +101,7 @@ func TestLightClientAttackEvidenceBasic(t *testing.T) {
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
extCommit, err := MakeExtCommit(blockID, height, 1, voteSet, privVals, defaultVoteTime)
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
@@ -163,7 +163,7 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) {
blockID := makeBlockID(header.Hash(), math.MaxInt32, tmhash.Sum([]byte("partshash")))
extCommit, err := MakeExtCommit(blockID, height, 1, voteSet, privVals, time.Now())
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
+24
View File
@@ -37,6 +37,7 @@ type ConsensusParams struct {
Evidence EvidenceParams `json:"evidence"`
Validator ValidatorParams `json:"validator"`
Version VersionParams `json:"version"`
ABCI ABCIParams `json:"abci"`
}
// BlockParams define limits on the block size and gas plus minimum time
@@ -63,6 +64,21 @@ type VersionParams struct {
App uint64 `json:"app"`
}
// ABCIParams configure ABCI functionality specific to the Application Blockchain
// Interface.
type ABCIParams struct {
VoteExtensionsEnableHeight int64 `json:"vote_extensions_enable_height"`
}
// VoteExtensionsEnabled returns true if vote extensions are enabled at height h
// and false otherwise.
func (a ABCIParams) VoteExtensionsEnabled(h int64) bool {
if a.VoteExtensionsEnableHeight == 0 {
return false
}
return a.VoteExtensionsEnableHeight <= h
}
// DefaultConsensusParams returns a default ConsensusParams.
func DefaultConsensusParams() *ConsensusParams {
return &ConsensusParams{
@@ -70,6 +86,7 @@ func DefaultConsensusParams() *ConsensusParams {
Evidence: DefaultEvidenceParams(),
Validator: DefaultValidatorParams(),
Version: DefaultVersionParams(),
ABCI: DefaultABCIParams(),
}
}
@@ -104,6 +121,13 @@ func DefaultVersionParams() VersionParams {
}
}
func DefaultABCIParams() ABCIParams {
return ABCIParams{
// When set to 0, vote extensions are not required.
VoteExtensionsEnableHeight: 0,
}
}
func IsValidPubkeyType(params ValidatorParams, pubkeyType string) bool {
for i := 0; i < len(params.PubKeyTypes); i++ {
if params.PubKeyTypes[i] == pubkeyType {
+5 -5
View File
@@ -145,7 +145,7 @@ func TestValidatorSet_VerifyCommit_CheckAllSignatures(t *testing.T) {
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
extCommit, err := MakeExtCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
// malleate 4th signature
@@ -173,7 +173,7 @@ func TestValidatorSet_VerifyCommitLight_ReturnsAsSoonAsMajorityOfVotingPowerSign
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
extCommit, err := MakeExtCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
// malleate 4th signature (3 signatures are enough for 2/3+)
@@ -199,7 +199,7 @@ func TestValidatorSet_VerifyCommitLightTrusting_ReturnsAsSoonAsTrustLevelOfVotin
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
extCommit, err := MakeExtCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
// malleate 3rd signature (2 signatures are enough for 1/3+ trust level)
@@ -223,7 +223,7 @@ func TestValidatorSet_VerifyCommitLightTrusting(t *testing.T) {
newValSet, _ = RandValidatorSet(2, 1)
)
require.NoError(t, err)
commit := extCommit.StripExtensions()
commit := extCommit.ToCommit()
testCases := []struct {
valSet *ValidatorSet
@@ -265,7 +265,7 @@ func TestValidatorSet_VerifyCommitLightTrustingErrorsOnOverflow(t *testing.T) {
)
require.NoError(t, err)
err = valSet.VerifyCommitLightTrusting("test_chain_id", extCommit.StripExtensions(),
err = valSet.VerifyCommitLightTrusting("test_chain_id", extCommit.ToCommit(),
tmmath.Fraction{Numerator: 25, Denominator: 55})
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "int64 overflow")
+47 -24
View File
@@ -27,7 +27,7 @@ var (
ErrVoteInvalidBlockHash = errors.New("invalid block hash")
ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature")
ErrVoteNil = errors.New("nil vote")
ErrVoteInvalidExtension = errors.New("invalid vote extension")
ErrVoteExtensionAbsent = errors.New("vote extension absent")
)
type ErrVoteConflictingVotes struct {
@@ -112,6 +112,16 @@ func (vote *Vote) CommitSig() CommitSig {
}
}
// StripExtension removes any extension data from the vote. Useful if the
// chain has not enabled vote extensions.
// Returns true if extension data was present before stripping and false otherwise.
func (vote *Vote) StripExtension() bool {
stripped := len(vote.Extension) > 0 || len(vote.ExtensionSignature) > 0
vote.Extension = nil
vote.ExtensionSignature = nil
return stripped
}
// ExtendedCommitSig attempts to construct an ExtendedCommitSig from this vote.
// Panics if either the vote extension signature is missing or if the block ID
// is not either empty or complete.
@@ -120,13 +130,8 @@ func (vote *Vote) ExtendedCommitSig() ExtendedCommitSig {
return NewExtendedCommitSigAbsent()
}
cs := vote.CommitSig()
if vote.BlockID.IsComplete() && len(vote.ExtensionSignature) == 0 {
panic(fmt.Sprintf("Invalid vote %v - BlockID is complete but missing vote extension signature", vote))
}
return ExtendedCommitSig{
CommitSig: cs,
CommitSig: vote.CommitSig(),
Extension: vote.Extension,
ExtensionSignature: vote.ExtensionSignature,
}
@@ -230,11 +235,11 @@ func (vote *Vote) Verify(chainID string, pubKey crypto.PubKey) error {
return err
}
// VerifyWithExtension performs the same verification as Verify, but
// VerifyVoteAndExtension 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 {
func (vote *Vote) VerifyVoteAndExtension(chainID string, pubKey crypto.PubKey) error {
v, err := vote.verifyAndReturnProto(chainID, pubKey)
if err != nil {
return err
@@ -249,6 +254,20 @@ func (vote *Vote) VerifyWithExtension(chainID string, pubKey crypto.PubKey) erro
return nil
}
// VerifyExtension checks whether the vote extension signature corresponds to the
// given chain ID and public key.
func (vote *Vote) VerifyExtension(chainID string, pubKey crypto.PubKey) error {
if vote.Type != tmproto.PrecommitType || len(vote.BlockID.Hash) == 0 {
return nil
}
v := vote.ToProto()
extSignBytes := VoteExtensionSignBytes(chainID, v)
if !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.
@@ -306,30 +325,34 @@ func (vote *Vote) ValidateBasic() error {
}
}
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 non-nil precommits
if vote.Type == tmproto.PrecommitType && len(vote.BlockID.Hash) != 0 {
if 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)
}
if len(vote.ExtensionSignature) == 0 && len(vote.Extension) != 0 {
return fmt.Errorf("vote extension signature absent on vote with extension")
}
}
return nil
}
// EnsureExtension checks for the presence of extensions signature data
// on precommit vote types.
func (vote *Vote) EnsureExtension() error {
// We should always see vote extension signatures in non-nil precommits
if vote.Type != tmproto.PrecommitType {
return nil
}
if len(vote.BlockID.Hash) == 0 {
return nil
}
if len(vote.ExtensionSignature) > 0 {
return nil
}
return ErrVoteExtensionAbsent
}
// 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 {
+30 -8
View File
@@ -2,6 +2,7 @@ package types
import (
"bytes"
"errors"
"fmt"
"strings"
@@ -59,11 +60,12 @@ there's only a limited number of peers.
NOTE: Assumes that the sum total of voting power does not exceed MaxUInt64.
*/
type VoteSet struct {
chainID string
height int64
round int32
signedMsgType tmproto.SignedMsgType
valSet *ValidatorSet
chainID string
height int64
round int32
signedMsgType tmproto.SignedMsgType
valSet *ValidatorSet
extensionsEnabled bool
mtx tmsync.Mutex
votesBitArray *bits.BitArray
@@ -74,7 +76,8 @@ type VoteSet struct {
peerMaj23s map[P2PID]BlockID // Maj23 for each peer
}
// Constructs a new VoteSet struct used to accumulate votes for given height/round.
// NewVoteSet instantiates all fields of a new vote set. This constructor requires
// that no vote extension data be present on the votes that are added to the set.
func NewVoteSet(chainID string, height int64, round int32,
signedMsgType tmproto.SignedMsgType, valSet *ValidatorSet) *VoteSet {
if height == 0 {
@@ -95,6 +98,16 @@ func NewVoteSet(chainID string, height int64, round int32,
}
}
// NewExtendedVoteSet constructs a vote set with additional vote verification logic.
// The VoteSet constructed with NewExtendedVoteSet verifies the vote extension
// data for every vote added to the set.
func NewExtendedVoteSet(chainID string, height int64, round int32,
signedMsgType tmproto.SignedMsgType, valSet *ValidatorSet) *VoteSet {
vs := NewVoteSet(chainID, height, round, signedMsgType, valSet)
vs.extensionsEnabled = true
return vs
}
func (voteSet *VoteSet) ChainID() string {
return voteSet.chainID
}
@@ -202,8 +215,17 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) {
}
// Check signature.
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)
if voteSet.extensionsEnabled {
if err := vote.VerifyVoteAndExtension(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)
}
} else {
if err := vote.Verify(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)
}
if len(vote.ExtensionSignature) > 0 || len(vote.Extension) > 0 {
return false, errors.New("unexpected vote extension data present in vote")
}
}
// Add vote and get conflicting vote if any.
+88 -1
View File
@@ -2,6 +2,7 @@ package types
import (
"bytes"
"context"
"testing"
"github.com/stretchr/testify/assert"
@@ -475,6 +476,92 @@ func TestVoteSet_MakeCommit(t *testing.T) {
}
}
// TestVoteSet_VoteExtensionsEnabled tests that the vote set correctly validates
// vote extensions data when either required or not required.
func TestVoteSet_VoteExtensionsEnabled(t *testing.T) {
for _, tc := range []struct {
name string
requireExtensions bool
addExtension bool
exepectError bool
}{
{
name: "no extension but expected",
requireExtensions: true,
addExtension: false,
exepectError: true,
},
{
name: "invalid extensions but not expected",
requireExtensions: true,
addExtension: false,
exepectError: true,
},
{
name: "no extension and not expected",
requireExtensions: false,
addExtension: false,
exepectError: false,
},
{
name: "extension and expected",
requireExtensions: true,
addExtension: true,
exepectError: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
height, round := int64(1), int32(0)
valSet, privValidators := randValidatorPrivValSet(ctx, t, 5, 10)
var voteSet *VoteSet
if tc.requireExtensions {
voteSet = NewExtendedVoteSet("test_chain_id", height, round, tmproto.PrecommitType, valSet)
} else {
voteSet = NewVoteSet("test_chain_id", height, round, tmproto.PrecommitType, valSet)
}
val0 := privValidators[0]
val0p, err := val0.GetPubKey(ctx)
require.NoError(t, err)
val0Addr := val0p.Address()
blockHash := crypto.CRandBytes(32)
blockPartsTotal := uint32(123)
blockPartSetHeader := PartSetHeader{blockPartsTotal, crypto.CRandBytes(32)}
vote := &Vote{
ValidatorAddress: val0Addr,
ValidatorIndex: 0,
Height: height,
Round: round,
Type: tmproto.PrecommitType,
Timestamp: tmtime.Now(),
BlockID: BlockID{blockHash, blockPartSetHeader},
}
v := vote.ToProto()
err = val0.SignVote(ctx, voteSet.ChainID(), v)
require.NoError(t, err)
vote.Signature = v.Signature
if tc.addExtension {
vote.ExtensionSignature = v.ExtensionSignature
}
added, err := voteSet.AddVote(vote)
if tc.exepectError {
require.Error(t, err)
require.False(t, added)
} else {
require.NoError(t, err)
require.True(t, added)
}
})
}
}
// NOTE: privValidators are in order
func randVoteSet(
height int64,
@@ -484,7 +571,7 @@ func randVoteSet(
votingPower int64,
) (*VoteSet, *ValidatorSet, []PrivValidator) {
valSet, privValidators := RandValidatorSet(numValidators, votingPower)
return NewVoteSet("test_chain_id", height, round, signedMsgType, valSet), valSet, privValidators
return NewExtendedVoteSet("test_chain_id", height, round, signedMsgType, valSet), valSet, privValidators
}
// Convenience: Return new vote with different validator address/index
+38 -11
View File
@@ -1,6 +1,7 @@
package types
import (
"context"
"testing"
"time"
@@ -260,7 +261,7 @@ func TestVoteExtension(t *testing.T) {
if tc.includeSignature {
vote.ExtensionSignature = v.ExtensionSignature
}
err = vote.VerifyWithExtension("test_chain_id", pk)
err = vote.VerifyExtension("test_chain_id", pk)
if tc.expectError {
require.Error(t, err)
} else {
@@ -349,7 +350,7 @@ func TestValidVotes(t *testing.T) {
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)
require.NoError(t, tc.vote.EnsureExtension(), "EnsureExtension for %s", tc.name)
}
}
@@ -373,13 +374,13 @@ func TestInvalidVotes(t *testing.T) {
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)
require.NoError(t, prevote.EnsureExtension(), "EnsureExtension 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)
require.NoError(t, precommit.EnsureExtension(), "EnsureExtension for %s in invalid precommit", tc.name)
}
}
@@ -398,7 +399,7 @@ func TestInvalidPrevotes(t *testing.T) {
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)
require.NoError(t, prevote.EnsureExtension(), "EnsureExtension for %s", tc.name)
}
}
@@ -413,18 +414,44 @@ func TestInvalidPrecommitExtensions(t *testing.T) {
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)
// ValidateBasic ensures that vote extensions, if present, are well formed
require.Error(t, precommit.ValidateBasic(), "ValidateBasic for %s", tc.name)
}
}
func TestEnsureVoteExtension(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
privVal := NewMockPV()
testCases := []struct {
name string
malleateVote func(*Vote)
expectError bool
}{
{"vote extension signature absent", func(v *Vote) {
v.Extension = nil
v.ExtensionSignature = nil
}, true},
{"vote extension signature present", func(v *Vote) {
v.ExtensionSignature = []byte("extension signature")
}, false},
}
for _, tc := range testCases {
precommit := examplePrecommit(t)
signVote(ctx, t, privVal, "test_chain_id", precommit)
tc.malleateVote(precommit)
if tc.expectError {
require.Error(t, precommit.EnsureExtension(), "EnsureExtension for %s", tc.name)
} else {
require.NoError(t, precommit.EnsureExtension(), "EnsureExtension for %s", tc.name)
}
}
}