implement vote extensions

This commit is contained in:
Callum Waters
2022-10-13 10:15:55 +02:00
parent 1e9f0afbe6
commit f809a0f1a2
46 changed files with 3397 additions and 1060 deletions
+374 -3
View File
@@ -875,7 +875,7 @@ func (commit *Commit) ValidateBasic() error {
}
if commit.Height >= 1 {
if commit.BlockID.IsZero() {
if commit.BlockID.IsNil() {
return errors.New("commit cannot be for nil block")
}
@@ -987,6 +987,360 @@ func CommitFromProto(cp *tmproto.Commit) (*Commit, error) {
return commit, commit.ValidateBasic()
}
//-------------------------------------
// ExtendedCommitSig contains a commit signature along with its corresponding
// vote extension and vote extension signature.
type ExtendedCommitSig struct {
CommitSig // Commit signature
Extension []byte // Vote extension
ExtensionSignature []byte // Vote extension signature
}
// NewExtendedCommitSigAbsent returns new ExtendedCommitSig with
// BlockIDFlagAbsent. Other fields are all empty.
func NewExtendedCommitSigAbsent() ExtendedCommitSig {
return ExtendedCommitSig{CommitSig: NewCommitSigAbsent()}
}
// String returns a string representation of an ExtendedCommitSig.
//
// 1. commit sig
// 2. first 6 bytes of vote extension
// 3. first 6 bytes of vote extension signature
func (ecs ExtendedCommitSig) String() string {
return fmt.Sprintf("ExtendedCommitSig{%s with %X %X}",
ecs.CommitSig,
tmbytes.Fingerprint(ecs.Extension),
tmbytes.Fingerprint(ecs.ExtensionSignature),
)
}
// ValidateBasic checks whether the structure is well-formed.
func (ecs ExtendedCommitSig) ValidateBasic() error {
if err := ecs.CommitSig.ValidateBasic(); err != nil {
return err
}
if ecs.BlockIDFlag == BlockIDFlagCommit {
if len(ecs.Extension) > MaxVoteExtensionSize {
return fmt.Errorf("vote extension is too big (max: %d)", MaxVoteExtensionSize)
}
if len(ecs.ExtensionSignature) > MaxSignatureSize {
return fmt.Errorf("vote extension signature is too big (max: %d)", MaxSignatureSize)
}
return nil
}
if len(ecs.ExtensionSignature) == 0 && len(ecs.Extension) != 0 {
return errors.New("vote extension signature absent on vote with extension")
}
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
}
// ToProto converts the ExtendedCommitSig to its Protobuf representation.
func (ecs *ExtendedCommitSig) ToProto() *tmproto.ExtendedCommitSig {
if ecs == nil {
return nil
}
return &tmproto.ExtendedCommitSig{
BlockIdFlag: tmproto.BlockIDFlag(ecs.BlockIDFlag),
ValidatorAddress: ecs.ValidatorAddress,
Timestamp: ecs.Timestamp,
Signature: ecs.Signature,
Extension: ecs.Extension,
ExtensionSignature: ecs.ExtensionSignature,
}
}
// FromProto populates the ExtendedCommitSig with values from the given
// Protobuf representation. Returns an error if the ExtendedCommitSig is
// invalid.
func (ecs *ExtendedCommitSig) FromProto(ecsp tmproto.ExtendedCommitSig) error {
ecs.BlockIDFlag = BlockIDFlag(ecsp.BlockIdFlag)
ecs.ValidatorAddress = ecsp.ValidatorAddress
ecs.Timestamp = ecsp.Timestamp
ecs.Signature = ecsp.Signature
ecs.Extension = ecsp.Extension
ecs.ExtensionSignature = ecsp.ExtensionSignature
return ecs.ValidateBasic()
}
//-------------------------------------
// ExtendedCommit is similar to Commit, except that its signatures also retain
// their corresponding vote extensions and vote extension signatures.
type ExtendedCommit struct {
Height int64
Round int32
BlockID BlockID
ExtendedSignatures []ExtendedCommitSig
bitArray *bits.BitArray
}
// Clone creates a deep copy of this extended commit.
func (ec *ExtendedCommit) Clone() *ExtendedCommit {
sigs := make([]ExtendedCommitSig, len(ec.ExtendedSignatures))
copy(sigs, ec.ExtendedSignatures)
ecc := *ec
ecc.ExtendedSignatures = sigs
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 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.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.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
}
// 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) ToCommit() *Commit {
cs := make([]CommitSig, len(ec.ExtendedSignatures))
for idx, ecs := range ec.ExtendedSignatures {
cs[idx] = ecs.CommitSig
}
return &Commit{
Height: ec.Height,
Round: ec.Round,
BlockID: ec.BlockID,
Signatures: cs,
}
}
// GetExtendedVote converts the ExtendedCommitSig for the given validator
// index to a Vote with a vote extensions.
// It panics if valIndex is out of range.
func (ec *ExtendedCommit) GetExtendedVote(valIndex int32) *Vote {
ecs := ec.ExtendedSignatures[valIndex]
return &Vote{
Type: tmproto.PrecommitType,
Height: ec.Height,
Round: ec.Round,
BlockID: ecs.BlockID(ec.BlockID),
Timestamp: ecs.Timestamp,
ValidatorAddress: ecs.ValidatorAddress,
ValidatorIndex: valIndex,
Signature: ecs.Signature,
Extension: ecs.Extension,
ExtensionSignature: ecs.ExtensionSignature,
}
}
// Type returns the vote type of the extended commit, which is always
// VoteTypePrecommit
// Implements VoteSetReader.
func (ec *ExtendedCommit) Type() byte { return byte(tmproto.PrecommitType) }
// GetHeight returns height of the extended commit.
// Implements VoteSetReader.
func (ec *ExtendedCommit) GetHeight() int64 { return ec.Height }
// GetRound returns height of the extended commit.
// Implements VoteSetReader.
func (ec *ExtendedCommit) GetRound() int32 { return ec.Round }
// Size returns the number of signatures in the extended commit.
// Implements VoteSetReader.
func (ec *ExtendedCommit) Size() int {
if ec == nil {
return 0
}
return len(ec.ExtendedSignatures)
}
// BitArray returns a BitArray of which validators voted for BlockID or nil in
// this extended commit.
// Implements VoteSetReader.
func (ec *ExtendedCommit) BitArray() *bits.BitArray {
if ec.bitArray == nil {
ec.bitArray = bits.NewBitArray(len(ec.ExtendedSignatures))
for i, extCommitSig := range ec.ExtendedSignatures {
// TODO: need to check the BlockID otherwise we could be counting conflicts,
// not just the one with +2/3 !
ec.bitArray.SetIndex(i, extCommitSig.BlockIDFlag != BlockIDFlagAbsent)
}
}
return ec.bitArray
}
// GetByIndex returns the vote corresponding to a given validator index.
// Panics if `index >= extCommit.Size()`.
// Implements VoteSetReader.
func (ec *ExtendedCommit) GetByIndex(valIdx int32) *Vote {
return ec.GetExtendedVote(valIdx)
}
// IsCommit returns true if there is at least one signature.
// Implements VoteSetReader.
func (ec *ExtendedCommit) IsCommit() bool {
return len(ec.ExtendedSignatures) != 0
}
// ValidateBasic checks whether the extended commit is well-formed. Does not
// actually check the cryptographic signatures.
func (ec *ExtendedCommit) ValidateBasic() error {
if ec.Height < 0 {
return errors.New("negative Height")
}
if ec.Round < 0 {
return errors.New("negative Round")
}
if ec.Height >= 1 {
if ec.BlockID.IsNil() {
return errors.New("commit cannot be for nil block")
}
if len(ec.ExtendedSignatures) == 0 {
return errors.New("no signatures in commit")
}
for i, extCommitSig := range ec.ExtendedSignatures {
if err := extCommitSig.ValidateBasic(); err != nil {
return fmt.Errorf("wrong ExtendedCommitSig #%d: %v", i, err)
}
}
}
return nil
}
// ToProto converts ExtendedCommit to protobuf
func (ec *ExtendedCommit) ToProto() *tmproto.ExtendedCommit {
if ec == nil {
return nil
}
c := new(tmproto.ExtendedCommit)
sigs := make([]tmproto.ExtendedCommitSig, len(ec.ExtendedSignatures))
for i := range ec.ExtendedSignatures {
sigs[i] = *ec.ExtendedSignatures[i].ToProto()
}
c.ExtendedSignatures = sigs
c.Height = ec.Height
c.Round = ec.Round
c.BlockID = ec.BlockID.ToProto()
return c
}
// ExtendedCommitFromProto constructs an ExtendedCommit from the given Protobuf
// representation. It returns an error if the extended commit is invalid.
func ExtendedCommitFromProto(ecp *tmproto.ExtendedCommit) (*ExtendedCommit, error) {
if ecp == nil {
return nil, errors.New("nil ExtendedCommit")
}
extCommit := new(ExtendedCommit)
bi, err := BlockIDFromProto(&ecp.BlockID)
if err != nil {
return nil, err
}
sigs := make([]ExtendedCommitSig, len(ecp.ExtendedSignatures))
for i := range ecp.ExtendedSignatures {
if err := sigs[i].FromProto(ecp.ExtendedSignatures[i]); err != nil {
return nil, err
}
}
extCommit.ExtendedSignatures = sigs
extCommit.Height = ecp.Height
extCommit.Round = ecp.Round
extCommit.BlockID = *bi
return extCommit, extCommit.ValidateBasic()
}
//-----------------------------------------------------------------------------
// Data contains the set of transactions included in the block
@@ -1167,6 +1521,17 @@ type BlockID struct {
PartSetHeader PartSetHeader `json:"parts"`
}
func NewBlockID(hash []byte, header PartSetHeader) BlockID {
return BlockID{
Hash: hash,
PartSetHeader: header,
}
}
func NilBlockID() BlockID {
return NewBlockID(nil, PartSetHeader{})
}
// Equals returns true if the BlockID matches the given BlockID
func (blockID BlockID) Equals(other BlockID) bool {
return bytes.Equal(blockID.Hash, other.Hash) &&
@@ -1196,8 +1561,8 @@ func (blockID BlockID) ValidateBasic() error {
return nil
}
// IsZero returns true if this is the BlockID of a nil block.
func (blockID BlockID) IsZero() bool {
// IsNil returns true if this is the BlockID of a nil block.
func (blockID BlockID) IsNil() bool {
return len(blockID.Hash) == 0 &&
blockID.PartSetHeader.IsZero()
}
@@ -1249,3 +1614,9 @@ func BlockIDFromProto(bID *tmproto.BlockID) (*BlockID, error) {
return blockID, blockID.ValidateBasic()
}
// IsProtoBlockIDNil is similar to the IsNil function on BlockID, but for the
// Protobuf representation.
func IsProtoBlockIDNil(bID *tmproto.BlockID) bool {
return len(bID.Hash) == 0 && IsProtoPartSetHeaderZero(&bID.PartSetHeader)
}
+147 -15
View File
@@ -38,14 +38,14 @@ func TestBlockAddEvidence(t *testing.T) {
h := int64(3)
voteSet, _, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
require.NoError(t, err)
ev, err := NewMockDuplicateVoteEvidenceWithValidator(h, time.Now(), vals[0], "block-test-chain")
require.NoError(t, err)
evList := []Evidence{ev}
block := MakeBlock(h, txs, commit, 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)
@@ -59,8 +59,9 @@ func TestBlockValidateBasic(t *testing.T) {
h := int64(3)
voteSet, valSet, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.ToCommit()
ev, err := NewMockDuplicateVoteEvidenceWithValidator(h, time.Now(), vals[0], "block-test-chain")
require.NoError(t, err)
@@ -131,14 +132,14 @@ func TestBlockMakePartSetWithEvidence(t *testing.T) {
h := int64(3)
voteSet, _, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
require.NoError(t, err)
ev, err := NewMockDuplicateVoteEvidenceWithValidator(h, time.Now(), vals[0], "block-test-chain")
require.NoError(t, err)
evList := []Evidence{ev}
partSet, err := MakeBlock(h, []Tx{Tx("Hello World")}, commit, evList).MakePartSet(512)
partSet, err := MakeBlock(h, []Tx{Tx("Hello World")}, extCommit.ToCommit(), evList).MakePartSet(512)
require.NoError(t, err)
assert.NotNil(t, partSet)
@@ -151,14 +152,14 @@ func TestBlockHashesTo(t *testing.T) {
lastID := makeBlockIDRandom()
h := int64(3)
voteSet, valSet, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
require.NoError(t, err)
ev, err := NewMockDuplicateVoteEvidenceWithValidator(h, time.Now(), vals[0], "block-test-chain")
require.NoError(t, err)
evList := []Evidence{ev}
block := MakeBlock(h, []Tx{Tx("Hello World")}, commit, 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")))
@@ -230,7 +231,7 @@ func TestCommit(t *testing.T) {
lastID := makeBlockIDRandom()
h := int64(3)
voteSet, _, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
commit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
require.NoError(t, err)
assert.Equal(t, h-1, commit.Height)
@@ -438,11 +439,11 @@ func randCommit(now time.Time) *Commit {
lastID := makeBlockIDRandom()
h := int64(3)
voteSet, _, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, now)
extCommit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, now)
if err != nil {
panic(err)
}
return commit
return extCommit.ToCommit()
}
func hexBytesFromString(s string) bytes.HexBytes {
@@ -514,13 +515,144 @@ 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()
_, valSet, vals := randVoteSet(10, 1, tmproto.PrecommitType, 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()
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(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) {
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)
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
}
}
chainID := voteSet.ChainID()
var voteSet2 *VoteSet
if testCase.includeExtension {
voteSet2 = extCommit.ToExtendedVoteSet(chainID, valSet)
} else {
voteSet2 = extCommit.ToVoteSet(chainID, valSet)
}
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)
}
})
}
}
func TestCommitToVoteSet(t *testing.T) {
lastID := makeBlockIDRandom()
h := int64(3)
voteSet, valSet, vals := randVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(lastID, h-1, 1, voteSet, vals, time.Now())
assert.NoError(t, err)
commit := extCommit.ToCommit()
chainID := voteSet.ChainID()
voteSet2 := CommitToVoteSet(chainID, commit, valSet)
@@ -587,12 +719,12 @@ func TestCommitToVoteSetWithVotesForNilBlock(t *testing.T) {
}
if tc.valid {
commit := voteSet.MakeCommit() // panics without > 2/3 valid votes
assert.NotNil(t, commit)
err := valSet.VerifyCommit(voteSet.ChainID(), blockID, height-1, commit)
extCommit := voteSet.MakeExtendedCommit() // panics without > 2/3 valid votes
assert.NotNil(t, extCommit)
err := valSet.VerifyCommit(voteSet.ChainID(), blockID, height-1, extCommit.ToCommit())
assert.Nil(t, err)
} else {
assert.Panics(t, func() { voteSet.MakeCommit() })
assert.Panics(t, func() { voteSet.MakeExtendedCommit() })
}
}
}
+13 -1
View File
@@ -21,7 +21,7 @@ func CanonicalizeBlockID(bid tmproto.BlockID) *tmproto.CanonicalBlockID {
panic(err)
}
var cbid *tmproto.CanonicalBlockID
if rbid == nil || rbid.IsZero() {
if rbid == nil || rbid.IsNil() {
cbid = nil
} else {
cbid = &tmproto.CanonicalBlockID{
@@ -64,6 +64,18 @@ func CanonicalizeVote(chainID string, vote *tmproto.Vote) tmproto.CanonicalVote
}
}
// CanonicalizeVoteExtension extracts the vote extension from the given vote
// and constructs a CanonicalizeVoteExtension struct, whose representation in
// bytes is what is signed in order to produce the vote extension's signature.
func CanonicalizeVoteExtension(chainID string, vote *tmproto.Vote) tmproto.CanonicalVoteExtension {
return tmproto.CanonicalVoteExtension{
Extension: vote.Extension,
Height: vote.Height,
Round: int64(vote.Round),
ChainId: chainID,
}
}
// CanonicalTime can be used to stringify time in a canonical way.
func CanonicalTime(t time.Time) string {
// Note that sending time over amino resets it to
+5 -4
View File
@@ -99,8 +99,9 @@ func TestLightClientAttackEvidenceBasic(t *testing.T) {
header := makeHeaderRandom()
header.Height = height
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
commit, err := MakeCommit(blockID, height, 1, voteSet, privVals, defaultVoteTime)
extCommit, err := makeExtCommit(blockID, height, 1, voteSet, privVals, defaultVoteTime)
require.NoError(t, err)
commit := extCommit.ToCommit()
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
@@ -159,13 +160,13 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) {
header.Height = height
header.ValidatorsHash = valSet.Hash()
blockID := makeBlockID(header.Hash(), math.MaxInt32, tmhash.Sum([]byte("partshash")))
commit, err := MakeCommit(blockID, height, 1, voteSet, privVals, time.Now())
extCommit, err := makeExtCommit(blockID, height, 1, voteSet, privVals, time.Now())
require.NoError(t, err)
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
Commit: extCommit.ToCommit(),
},
ValidatorSet: valSet,
},
@@ -205,7 +206,7 @@ func TestLightClientAttackEvidenceValidation(t *testing.T) {
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
Commit: extCommit.ToCommit(),
},
ValidatorSet: valSet,
},
+73 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/crypto/tmhash"
tmstrings "github.com/tendermint/tendermint/libs/strings"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
@@ -37,6 +38,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 +65,22 @@ 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"`
RecheckTx bool `json:"recheck_tx"`
}
// 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 +88,7 @@ func DefaultConsensusParams() *ConsensusParams {
Evidence: DefaultEvidenceParams(),
Validator: DefaultValidatorParams(),
Version: DefaultVersionParams(),
ABCI: DefaultABCIParams(),
}
}
@@ -104,6 +123,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 {
@@ -150,6 +176,10 @@ func (params ConsensusParams) ValidateBasic() error {
params.Evidence.MaxBytes)
}
if params.ABCI.VoteExtensionsEnableHeight < 0 {
return fmt.Errorf("ABCI.VoteExtensionsEnableHeight cannot be negative. Got: %d", params.ABCI.VoteExtensionsEnableHeight)
}
if len(params.Validator.PubKeyTypes) == 0 {
return errors.New("len(Validator.PubKeyTypes) must be greater than 0")
}
@@ -166,6 +196,30 @@ func (params ConsensusParams) ValidateBasic() error {
return nil
}
func (params ConsensusParams) ValidateUpdate(updated *tmproto.ConsensusParams, h int64) error {
if updated.Abci == nil {
return nil
}
if params.ABCI.VoteExtensionsEnableHeight == updated.Abci.VoteExtensionsEnableHeight {
return nil
}
if params.ABCI.VoteExtensionsEnableHeight != 0 && updated.Abci.VoteExtensionsEnableHeight == 0 {
return errors.New("vote extensions cannot be disabled once enabled")
}
if updated.Abci.VoteExtensionsEnableHeight <= h {
return fmt.Errorf("VoteExtensionsEnableHeight cannot be updated to a past height, "+
"initial height: %d, current height %d",
params.ABCI.VoteExtensionsEnableHeight, h)
}
if params.ABCI.VoteExtensionsEnableHeight <= h {
return fmt.Errorf("VoteExtensionsEnableHeight cannot be updated modified once"+
"the initial height has occurred, "+
"initial height: %d, current height %d",
params.ABCI.VoteExtensionsEnableHeight, h)
}
return nil
}
// Hash returns a hash of a subset of the parameters to store in the block header.
// Only the Block.MaxBytes and Block.MaxGas are included in the hash.
// This allows the ConsensusParams to evolve more without breaking the block
@@ -190,6 +244,14 @@ func (params ConsensusParams) Hash() []byte {
return hasher.Sum(nil)
}
func (params *ConsensusParams) Equals(params2 *ConsensusParams) bool {
return params.Block == params2.Block &&
params.Evidence == params2.Evidence &&
params.Version == params2.Version &&
params.ABCI == params2.ABCI &&
tmstrings.StringSliceEqual(params.Validator.PubKeyTypes, params2.Validator.PubKeyTypes)
}
// Update returns a copy of the params with updates from the non-zero fields of p2.
// NOTE: note: must not modify the original
func (params ConsensusParams) Update(params2 *tmproto.ConsensusParams) ConsensusParams {
@@ -217,6 +279,9 @@ func (params ConsensusParams) Update(params2 *tmproto.ConsensusParams) Consensus
if params2.Version != nil {
res.Version.App = params2.Version.App
}
if params2.Abci != nil {
res.ABCI.VoteExtensionsEnableHeight = params2.Abci.GetVoteExtensionsEnableHeight()
}
return res
}
@@ -237,11 +302,14 @@ func (params *ConsensusParams) ToProto() tmproto.ConsensusParams {
Version: &tmproto.VersionParams{
App: params.Version.App,
},
Abci: &tmproto.ABCIParams{
VoteExtensionsEnableHeight: params.ABCI.VoteExtensionsEnableHeight,
},
}
}
func ConsensusParamsFromProto(pbParams tmproto.ConsensusParams) ConsensusParams {
return ConsensusParams{
c := ConsensusParams{
Block: BlockParams{
MaxBytes: pbParams.Block.MaxBytes,
MaxGas: pbParams.Block.MaxGas,
@@ -258,4 +326,8 @@ func ConsensusParamsFromProto(pbParams tmproto.ConsensusParams) ConsensusParams
App: pbParams.Version.App,
},
}
if pbParams.Abci != nil {
c.ABCI.VoteExtensionsEnableHeight = pbParams.Abci.GetVoteExtensionsEnableHeight()
}
return c
}
+6
View File
@@ -145,6 +145,12 @@ func PartSetHeaderFromProto(ppsh *tmproto.PartSetHeader) (*PartSetHeader, error)
return psh, psh.ValidateBasic()
}
// IsProtoPartSetHeaderZero is similar to the IsZero function for
// PartSetHeader, but for the Protobuf representation.
func IsProtoPartSetHeaderZero(ppsh *tmproto.PartSetHeader) bool {
return ppsh.Total == 0 && len(ppsh.Hash) == 0
}
//-------------------------------------
type PartSet struct {
+14
View File
@@ -82,6 +82,20 @@ func (pv MockPV) SignVote(chainID string, vote *tmproto.Vote) error {
return err
}
vote.Signature = sig
var extSig []byte
// We only sign vote extensions for non-nil precommits
// We always sign extensions, even if they are empty
if vote.Type == tmproto.PrecommitType && !IsProtoBlockIDNil(&vote.BlockID) {
extSignBytes := VoteExtensionSignBytes(useChainID, vote)
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 non-nil precommits")
}
vote.ExtensionSignature = extSig
return nil
}
+3 -3
View File
@@ -9,8 +9,8 @@ import (
"github.com/tendermint/tendermint/version"
)
func MakeCommit(blockID BlockID, height int64, round int32,
voteSet *VoteSet, validators []PrivValidator, now time.Time) (*Commit, error) {
func makeExtCommit(blockID BlockID, height int64, round int32,
voteSet *VoteSet, validators []PrivValidator, now time.Time) (*ExtendedCommit, error) {
// all sign
for i := 0; i < len(validators); i++ {
@@ -34,7 +34,7 @@ func MakeCommit(blockID BlockID, height int64, round int32,
}
}
return voteSet.MakeCommit(), nil
return voteSet.MakeExtendedCommit(), nil
}
func signAddVote(privVal PrivValidator, vote *Vote, voteSet *VoteSet) (signed bool, err error) {
+10 -5
View File
@@ -751,8 +751,9 @@ func TestValidatorSet_VerifyCommit_CheckAllSignatures(t *testing.T) {
)
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.ToCommit()
// malleate 4th signature
vote := voteSet.GetByIndex(3)
@@ -776,8 +777,9 @@ func TestValidatorSet_VerifyCommitLight_ReturnsAsSoonAsMajorityOfVotingPowerSign
)
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.ToCommit()
// malleate 4th signature (3 signatures are enough for 2/3+)
vote := voteSet.GetByIndex(3)
@@ -799,8 +801,9 @@ func TestValidatorSet_VerifyCommitLightTrusting_ReturnsAsSoonAsTrustLevelOfVotin
)
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now())
extCommit, err := makeExtCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
commit := extCommit.ToCommit()
// malleate 3rd signature (2 signatures are enough for 1/3+ trust level)
vote := voteSet.GetByIndex(2)
@@ -1521,10 +1524,11 @@ func TestValidatorSet_VerifyCommitLightTrusting(t *testing.T) {
var (
blockID = makeBlockIDRandom()
voteSet, originalValset, vals = randVoteSet(1, 1, tmproto.PrecommitType, 6, 1)
commit, err = MakeCommit(blockID, 1, 1, voteSet, vals, time.Now())
extCommit, err = makeExtCommit(blockID, 1, 1, voteSet, vals, time.Now())
newValSet, _ = RandValidatorSet(2, 1)
)
require.NoError(t, err)
commit := extCommit.ToCommit()
testCases := []struct {
valSet *ValidatorSet
@@ -1562,9 +1566,10 @@ func TestValidatorSet_VerifyCommitLightTrustingErrorsOnOverflow(t *testing.T) {
var (
blockID = makeBlockIDRandom()
voteSet, valSet, vals = randVoteSet(1, 1, tmproto.PrecommitType, 1, MaxTotalVotingPower)
commit, err = MakeCommit(blockID, 1, 1, voteSet, vals, time.Now())
extCommit, err = makeExtCommit(blockID, 1, 1, voteSet, vals, time.Now())
)
require.NoError(t, err)
commit := extCommit.ToCommit()
err = valSet.VerifyCommitLightTrusting("test_chain_id", commit,
tmmath.Fraction{Numerator: 25, Denominator: 55})
+196 -49
View File
@@ -13,7 +13,11 @@ import (
)
const (
nilVoteStr string = "nil-Vote"
absentVoteStr string = "Vote{absent}"
nilVoteStr string = "nil"
// The maximum supported number of bytes in a vote extension.
MaxVoteExtensionSize int = 1024 * 1024
)
var (
@@ -24,6 +28,7 @@ var (
ErrVoteInvalidBlockHash = errors.New("invalid block hash")
ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature")
ErrVoteNil = errors.New("nil vote")
ErrVoteExtensionAbsent = errors.New("expected vote extension is absent")
)
type ErrVoteConflictingVotes struct {
@@ -48,14 +53,16 @@ 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"`
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"`
Type tmproto.SignedMsgType `json:"type"`
Height int64 `json:"height"`
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"`
}
// CommitSig converts the Vote to a CommitSig.
@@ -68,7 +75,7 @@ func (vote *Vote) CommitSig() CommitSig {
switch {
case vote.BlockID.IsComplete():
blockIDFlag = BlockIDFlagCommit
case vote.BlockID.IsZero():
case vote.BlockID.IsNil():
blockIDFlag = BlockIDFlagNil
default:
panic(fmt.Sprintf("Invalid vote %v - expected BlockID to be either empty or complete", vote))
@@ -82,6 +89,31 @@ 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.
func (vote *Vote) ExtendedCommitSig() ExtendedCommitSig {
if vote == nil {
return NewExtendedCommitSigAbsent()
}
return ExtendedCommitSig{
CommitSig: vote.CommitSig(),
Extension: vote.Extension,
ExtensionSignature: vote.ExtensionSignature,
}
}
// VoteSignBytes returns the proto-encoding of the canonicalized Vote, for
// signing. Panics is the marshaling fails.
//
@@ -100,6 +132,21 @@ func VoteSignBytes(chainID string, vote *tmproto.Vote) []byte {
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
@@ -118,38 +165,91 @@ func (vote *Vote) Copy() *Vote {
// 9. timestamp
func (vote *Vote) String() string {
if vote == nil {
return nilVoteStr
return absentVoteStr
}
var blockHashString string
if len(vote.BlockID.Hash) > 0 {
blockHashString = fmt.Sprintf("%X", tmbytes.Fingerprint(vote.BlockID.Hash))
} else {
blockHashString = nilVoteStr
}
var typeString string
switch vote.Type {
case tmproto.PrevoteType:
typeString = "Prevote"
return fmt.Sprintf("Prevote{%v/%02d by %v:%X for %X <%X> @ %s}",
vote.Height,
vote.Round,
vote.ValidatorIndex,
tmbytes.Fingerprint(vote.ValidatorAddress),
blockHashString,
tmbytes.Fingerprint(vote.Signature),
CanonicalTime(vote.Timestamp),
)
case tmproto.PrecommitType:
typeString = "Precommit"
return fmt.Sprintf("Precommit{%v/%02d by %v:%X for %X <%X> & %d <%X> @ %s}",
vote.Height,
vote.Round,
vote.ValidatorIndex,
tmbytes.Fingerprint(vote.ValidatorAddress),
blockHashString,
tmbytes.Fingerprint(vote.Signature),
len(vote.Extension),
tmbytes.Fingerprint(vote.Extension),
CanonicalTime(vote.Timestamp),
)
default:
panic("Unknown vote type")
}
return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %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),
CanonicalTime(vote.Timestamp),
)
}
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 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
}
// 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) VerifyVoteAndExtension(chainID string, pubKey crypto.PubKey) error {
v, err := vote.verifyAndReturnProto(chainID, pubKey)
if err != nil {
return err
}
// We only verify vote extension signatures for non-nil precommits.
if vote.Type == tmproto.PrecommitType && !IsProtoBlockIDNil(&v.BlockID) {
extSignBytes := VoteExtensionSignBytes(chainID, v)
if !pubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
return ErrVoteInvalidSignature
}
}
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 || vote.BlockID.IsNil() {
return nil
}
v := vote.ToProto()
extSignBytes := VoteExtensionSignBytes(chainID, v)
if !pubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
return ErrVoteInvalidSignature
}
return nil
@@ -161,8 +261,8 @@ func (vote *Vote) ValidateBasic() error {
return errors.New("invalid Type")
}
if vote.Height < 0 {
return errors.New("negative Height")
if vote.Height <= 0 {
return errors.New("negative or zero Height")
}
if vote.Round < 0 {
@@ -177,7 +277,7 @@ func (vote *Vote) ValidateBasic() error {
// BlockID.ValidateBasic would not err if we for instance have an empty hash but a
// non-empty PartsSetHeader:
if !vote.BlockID.IsZero() && !vote.BlockID.IsComplete() {
if !vote.BlockID.IsNil() && !vote.BlockID.IsComplete() {
return fmt.Errorf("blockID must be either empty or complete, got: %v", vote.BlockID)
}
@@ -198,9 +298,52 @@ func (vote *Vote) ValidateBasic() error {
return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize)
}
// We should only ever see vote extensions in non-nil precommits, otherwise
// this is a violation of the specification.
// https://github.com/tendermint/tendermint/issues/8487
if vote.Type != tmproto.PrecommitType || (vote.Type == tmproto.PrecommitType && vote.BlockID.IsNil()) {
if len(vote.Extension) > 0 {
return errors.New("unexpected vote extension")
}
if len(vote.ExtensionSignature) > 0 {
return errors.New("unexpected vote extension signature")
}
} else {
// It's possible that this vote has vote extensions but
// they could also be disabled and thus not present thus
// we can't do all checks
if len(vote.ExtensionSignature) > MaxSignatureSize {
return fmt.Errorf("vote extension signature is too big (max: %d)", MaxSignatureSize)
}
// NOTE: extended votes should have a signature regardless of
// of whether there is any data in the extension or not however
// we don't know if extensions are enabled so we can only
// enforce the signature when extension size is no nil
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 vote.BlockID.IsNil() {
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 {
@@ -209,14 +352,16 @@ func (vote *Vote) ToProto() *tmproto.Vote {
}
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,
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,
}
}
@@ -248,15 +393,17 @@ func VoteFromProto(pv *tmproto.Vote) (*Vote, error) {
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 := &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,
}
return vote, vote.ValidateBasic()
}
+33 -17
View File
@@ -59,11 +59,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
@@ -95,6 +96,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
}
@@ -611,36 +622,41 @@ func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) {
//--------------------------------------------------------------------------------
// Commit
// MakeCommit constructs a Commit from the VoteSet. It only includes precommits
// for the block, which has 2/3+ majority, and nil.
// MakeExtendedCommit constructs a Commit from the VoteSet. It only includes
// precommits for the block, which has 2/3+ majority, and nil.
//
// Panics if the vote type is not PrecommitType or if there's no +2/3 votes for
// a single block.
func (voteSet *VoteSet) MakeCommit() *Commit {
func (voteSet *VoteSet) MakeExtendedCommit() *ExtendedCommit {
if voteSet.signedMsgType != tmproto.PrecommitType {
panic("Cannot MakeCommit() unless VoteSet.Type is PrecommitType")
panic("Cannot MakeExtendCommit() unless VoteSet.Type is PrecommitType")
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
// Make sure we have a 2/3 majority
if voteSet.maj23 == nil {
panic("Cannot MakeCommit() unless a blockhash has +2/3")
panic("Cannot MakeExtendCommit() unless a blockhash has +2/3")
}
// For every validator, get the precommit
commitSigs := make([]CommitSig, len(voteSet.votes))
// For every validator, get the precommit with extensions
sigs := make([]ExtendedCommitSig, len(voteSet.votes))
for i, v := range voteSet.votes {
commitSig := v.CommitSig()
sig := v.ExtendedCommitSig()
// if block ID exists but doesn't match, exclude sig
if commitSig.ForBlock() && !v.BlockID.Equals(*voteSet.maj23) {
commitSig = NewCommitSigAbsent()
if sig.BlockIDFlag == BlockIDFlagCommit && !v.BlockID.Equals(*voteSet.maj23) {
sig = NewExtendedCommitSigAbsent()
}
commitSigs[i] = commitSig
sigs[i] = sig
}
return NewCommit(voteSet.GetHeight(), voteSet.GetRound(), *voteSet.maj23, commitSigs)
return &ExtendedCommit{
Height: voteSet.GetHeight(),
Round: voteSet.GetRound(),
BlockID: *voteSet.maj23,
ExtendedSignatures: sigs,
}
}
//--------------------------------------------------------------------------------
+14 -14
View File
@@ -25,7 +25,7 @@ func TestVoteSet_AddVote_Good(t *testing.T) {
assert.Nil(t, voteSet.GetByAddress(val0Addr))
assert.False(t, voteSet.BitArray().GetIndex(0))
blockID, ok := voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(), "there should be no 2/3 majority")
assert.False(t, ok || !blockID.IsNil(), "there should be no 2/3 majority")
vote := &Vote{
ValidatorAddress: val0Addr,
@@ -42,7 +42,7 @@ func TestVoteSet_AddVote_Good(t *testing.T) {
assert.NotNil(t, voteSet.GetByAddress(val0Addr))
assert.True(t, voteSet.BitArray().GetIndex(0))
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(), "there should be no 2/3 majority")
assert.False(t, ok || !blockID.IsNil(), "there should be no 2/3 majority")
}
func TestVoteSet_AddVote_Bad(t *testing.T) {
@@ -143,7 +143,7 @@ func TestVoteSet_2_3Majority(t *testing.T) {
require.NoError(t, err)
}
blockID, ok := voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(), "there should be no 2/3 majority")
assert.False(t, ok || !blockID.IsNil(), "there should be no 2/3 majority")
// 7th validator voted for some blockhash
{
@@ -154,7 +154,7 @@ func TestVoteSet_2_3Majority(t *testing.T) {
_, err = signAddVote(privValidators[6], withBlockHash(vote, tmrand.Bytes(32)), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(), "there should be no 2/3 majority")
assert.False(t, ok || !blockID.IsNil(), "there should be no 2/3 majority")
}
// 8th validator voted for nil.
@@ -166,7 +166,7 @@ func TestVoteSet_2_3Majority(t *testing.T) {
_, err = signAddVote(privValidators[7], vote, voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.True(t, ok || blockID.IsZero(), "there should be 2/3 majority for nil")
assert.True(t, ok || blockID.IsNil(), "there should be 2/3 majority for nil")
}
}
@@ -198,7 +198,7 @@ func TestVoteSet_2_3MajorityRedux(t *testing.T) {
require.NoError(t, err)
}
blockID, ok := voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
assert.False(t, ok || !blockID.IsNil(),
"there should be no 2/3 majority")
// 67th validator voted for nil
@@ -210,7 +210,7 @@ func TestVoteSet_2_3MajorityRedux(t *testing.T) {
_, err = signAddVote(privValidators[66], withBlockHash(vote, nil), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
assert.False(t, ok || !blockID.IsNil(),
"there should be no 2/3 majority: last vote added was nil")
}
@@ -224,7 +224,7 @@ func TestVoteSet_2_3MajorityRedux(t *testing.T) {
_, err = signAddVote(privValidators[67], withBlockPartSetHeader(vote, blockPartsHeader), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
assert.False(t, ok || !blockID.IsNil(),
"there should be no 2/3 majority: last vote added had different PartSetHeader Hash")
}
@@ -238,7 +238,7 @@ func TestVoteSet_2_3MajorityRedux(t *testing.T) {
_, err = signAddVote(privValidators[68], withBlockPartSetHeader(vote, blockPartsHeader), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
assert.False(t, ok || !blockID.IsNil(),
"there should be no 2/3 majority: last vote added had different PartSetHeader Total")
}
@@ -251,7 +251,7 @@ func TestVoteSet_2_3MajorityRedux(t *testing.T) {
_, err = signAddVote(privValidators[69], withBlockHash(vote, tmrand.Bytes(32)), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
assert.False(t, ok || !blockID.IsNil(),
"there should be no 2/3 majority: last vote added had different BlockHash")
}
@@ -426,7 +426,7 @@ func TestVoteSet_MakeCommit(t *testing.T) {
}
// MakeCommit should fail.
assert.Panics(t, func() { voteSet.MakeCommit() }, "Doesn't have +2/3 majority")
assert.Panics(t, func() { voteSet.MakeExtendedCommit() }, "Doesn't have +2/3 majority")
// 7th voted for some other block.
{
@@ -463,13 +463,13 @@ func TestVoteSet_MakeCommit(t *testing.T) {
require.NoError(t, err)
}
commit := voteSet.MakeCommit()
extCommit := voteSet.MakeExtendedCommit()
// Commit should have 10 elements
assert.Equal(t, 10, len(commit.Signatures))
assert.Equal(t, 10, len(extCommit.ExtendedSignatures))
// Ensure that Commit is good.
if err := commit.ValidateBasic(); err != nil {
if err := extCommit.ValidateBasic(); err != nil {
t.Errorf("error in Commit.ValidateBasic(): %v", err)
}
}
+7 -3
View File
@@ -219,13 +219,13 @@ func TestVoteVerify(t *testing.T) {
func TestVoteString(t *testing.T) {
str := examplePrecommit().String()
expected := `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PRECOMMIT(Precommit) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests
expected := `Precommit{12345/02 by 56789:6AF1F4111082 for 384230313032333338364333 <000000000000> & 0 <000000000000> @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests
if str != expected {
t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str)
}
str2 := examplePrevote().String()
expected = `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PREVOTE(Prevote) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests
expected = `Prevote{12345/02 by 56789:6AF1F4111082 for 384230313032333338364333 <000000000000> @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests
if str2 != expected {
t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str2)
}
@@ -259,7 +259,11 @@ func TestVoteValidateBasic(t *testing.T) {
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")
if tc.expectErr {
require.Error(t, vote.ValidateBasic())
} else {
require.NoError(t, vote.ValidateBasic())
}
})
}
}