mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-19 06:31:57 +00:00
[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:
+96
-13
@@ -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
@@ -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() })
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user