pkg/block: move from types/

This commit is contained in:
Aleksandr Bezobchuk
2021-08-06 18:01:11 -04:00
parent e922016121
commit e5ffe132ae
43 changed files with 44 additions and 43 deletions
+1259
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
package block
import (
"bytes"
"errors"
"fmt"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// BlockMeta contains meta information.
type BlockMeta struct {
BlockID BlockID `json:"block_id"`
BlockSize int `json:"block_size"`
Header Header `json:"header"`
NumTxs int `json:"num_txs"`
}
// NewBlockMeta returns a new BlockMeta.
func NewBlockMeta(block *Block, blockParts *PartSet) *BlockMeta {
return &BlockMeta{
BlockID: BlockID{block.Hash(), blockParts.Header()},
BlockSize: block.Size(),
Header: block.Header,
NumTxs: len(block.Data.Txs),
}
}
func (bm *BlockMeta) ToProto() *tmproto.BlockMeta {
if bm == nil {
return nil
}
pb := &tmproto.BlockMeta{
BlockID: bm.BlockID.ToProto(),
BlockSize: int64(bm.BlockSize),
Header: *bm.Header.ToProto(),
NumTxs: int64(bm.NumTxs),
}
return pb
}
func BlockMetaFromProto(pb *tmproto.BlockMeta) (*BlockMeta, error) {
if pb == nil {
return nil, errors.New("blockmeta is empty")
}
bm := new(BlockMeta)
bi, err := BlockIDFromProto(&pb.BlockID)
if err != nil {
return nil, err
}
h, err := HeaderFromProto(&pb.Header)
if err != nil {
return nil, err
}
bm.BlockID = *bi
bm.BlockSize = int(pb.BlockSize)
bm.Header = h
bm.NumTxs = int(pb.NumTxs)
return bm, bm.ValidateBasic()
}
// ValidateBasic performs basic validation.
func (bm *BlockMeta) ValidateBasic() error {
if err := bm.BlockID.ValidateBasic(); err != nil {
return err
}
if !bytes.Equal(bm.BlockID.Hash, bm.Header.Hash()) {
return fmt.Errorf("expected BlockID#Hash and Header#Hash to be the same, got %X != %X",
bm.BlockID.Hash, bm.Header.Hash())
}
return nil
}
+95
View File
@@ -0,0 +1,95 @@
package block
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/tmhash"
tmrand "github.com/tendermint/tendermint/libs/rand"
)
func TestBlockMeta_ToProto(t *testing.T) {
h := MakeRandHeader()
bi := BlockID{Hash: h.Hash(), PartSetHeader: PartSetHeader{Total: 123, Hash: tmrand.Bytes(tmhash.Size)}}
bm := &BlockMeta{
BlockID: bi,
BlockSize: 200,
Header: h,
NumTxs: 0,
}
tests := []struct {
testName string
bm *BlockMeta
expErr bool
}{
{"success", bm, false},
{"failure nil", nil, true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.testName, func(t *testing.T) {
pb := tt.bm.ToProto()
bm, err := BlockMetaFromProto(pb)
if !tt.expErr {
require.NoError(t, err, tt.testName)
require.Equal(t, tt.bm, bm, tt.testName)
} else {
require.Error(t, err, tt.testName)
}
})
}
}
func TestBlockMeta_ValidateBasic(t *testing.T) {
h := MakeRandHeader()
bi := BlockID{Hash: h.Hash(), PartSetHeader: PartSetHeader{Total: 123, Hash: tmrand.Bytes(tmhash.Size)}}
bi2 := BlockID{Hash: tmrand.Bytes(tmhash.Size),
PartSetHeader: PartSetHeader{Total: 123, Hash: tmrand.Bytes(tmhash.Size)}}
bi3 := BlockID{Hash: []byte("incorrect hash"),
PartSetHeader: PartSetHeader{Total: 123, Hash: []byte("incorrect hash")}}
bm := &BlockMeta{
BlockID: bi,
BlockSize: 200,
Header: h,
NumTxs: 0,
}
bm2 := &BlockMeta{
BlockID: bi2,
BlockSize: 200,
Header: h,
NumTxs: 0,
}
bm3 := &BlockMeta{
BlockID: bi3,
BlockSize: 200,
Header: h,
NumTxs: 0,
}
tests := []struct {
name string
bm *BlockMeta
wantErr bool
}{
{"success", bm, false},
{"failure wrong blockID hash", bm2, true},
{"failure wrong length blockID hash", bm3, true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if err := tt.bm.ValidateBasic(); (err != nil) != tt.wantErr {
t.Errorf("BlockMeta.ValidateBasic() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
package block
import (
"time"
tmtime "github.com/tendermint/tendermint/libs/time"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// Canonical* wraps the structs in types for amino encoding them for use in SignBytes / the Signable interface.
// TimeFormat is used for generating the sigs
const TimeFormat = time.RFC3339Nano
//-----------------------------------
// Canonicalize the structs
func CanonicalizeBlockID(bid tmproto.BlockID) *tmproto.CanonicalBlockID {
rbid, err := BlockIDFromProto(&bid)
if err != nil {
panic(err)
}
var cbid *tmproto.CanonicalBlockID
if rbid == nil || rbid.IsZero() {
cbid = nil
} else {
cbid = &tmproto.CanonicalBlockID{
Hash: bid.Hash,
PartSetHeader: CanonicalizePartSetHeader(bid.PartSetHeader),
}
}
return cbid
}
// CanonicalizeVote transforms the given PartSetHeader to a CanonicalPartSetHeader.
func CanonicalizePartSetHeader(psh tmproto.PartSetHeader) tmproto.CanonicalPartSetHeader {
return tmproto.CanonicalPartSetHeader(psh)
}
// CanonicalizeVote transforms the given Proposal to a CanonicalProposal.
func CanonicalizeProposal(chainID string, proposal *tmproto.Proposal) tmproto.CanonicalProposal {
return tmproto.CanonicalProposal{
Type: tmproto.ProposalType,
Height: proposal.Height, // encoded as sfixed64
Round: int64(proposal.Round), // encoded as sfixed64
POLRound: int64(proposal.PolRound),
BlockID: CanonicalizeBlockID(proposal.BlockID),
Timestamp: proposal.Timestamp,
ChainID: chainID,
}
}
// CanonicalizeVote transforms the given Vote to a CanonicalVote, which does
// not contain ValidatorIndex and ValidatorAddress fields.
func CanonicalizeVote(chainID string, vote *tmproto.Vote) tmproto.CanonicalVote {
return tmproto.CanonicalVote{
Type: vote.Type,
Height: vote.Height, // encoded as sfixed64
Round: int64(vote.Round), // encoded as sfixed64
BlockID: CanonicalizeBlockID(vote.BlockID),
Timestamp: vote.Timestamp,
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
// local time, we need to force UTC here, so the
// signatures match
return tmtime.Canonical(t).Format(TimeFormat)
}
+39
View File
@@ -0,0 +1,39 @@
package block
import (
"reflect"
"testing"
"github.com/tendermint/tendermint/crypto/tmhash"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
func TestCanonicalizeBlockID(t *testing.T) {
randhash := tmrand.Bytes(tmhash.Size)
block1 := tmproto.BlockID{Hash: randhash,
PartSetHeader: tmproto.PartSetHeader{Total: 5, Hash: randhash}}
block2 := tmproto.BlockID{Hash: randhash,
PartSetHeader: tmproto.PartSetHeader{Total: 10, Hash: randhash}}
cblock1 := tmproto.CanonicalBlockID{Hash: randhash,
PartSetHeader: tmproto.CanonicalPartSetHeader{Total: 5, Hash: randhash}}
cblock2 := tmproto.CanonicalBlockID{Hash: randhash,
PartSetHeader: tmproto.CanonicalPartSetHeader{Total: 10, Hash: randhash}}
tests := []struct {
name string
args tmproto.BlockID
want *tmproto.CanonicalBlockID
}{
{"first", block1, &cblock1},
{"second", block2, &cblock2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if got := CanonicalizeBlockID(tt.args); !reflect.DeepEqual(got, tt.want) {
t.Errorf("CanonicalizeBlockID() = %v, want %v", got, tt.want)
}
})
}
}
+47
View File
@@ -0,0 +1,47 @@
package block
import (
gogotypes "github.com/gogo/protobuf/types"
"github.com/tendermint/tendermint/libs/bytes"
)
// cdcEncode returns nil if the input is nil, otherwise returns
// proto.Marshal(<type>Value{Value: item})
func cdcEncode(item interface{}) []byte {
if item != nil && !isTypedNil(item) && !isEmpty(item) {
switch item := item.(type) {
case string:
i := gogotypes.StringValue{
Value: item,
}
bz, err := i.Marshal()
if err != nil {
return nil
}
return bz
case int64:
i := gogotypes.Int64Value{
Value: item,
}
bz, err := i.Marshal()
if err != nil {
return nil
}
return bz
case bytes.HexBytes:
i := gogotypes.BytesValue{
Value: item,
}
bz, err := i.Marshal()
if err != nil {
return nil
}
return bz
default:
return nil
}
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package block
import "fmt"
type (
// ErrInvalidCommitHeight is returned when we encounter a commit with an
// unexpected height.
ErrInvalidCommitHeight struct {
Expected int64
Actual int64
}
// ErrInvalidCommitSignatures is returned when we encounter a commit where
// the number of signatures doesn't match the number of validators.
ErrInvalidCommitSignatures struct {
Expected int
Actual int
}
)
func NewErrInvalidCommitHeight(expected, actual int64) ErrInvalidCommitHeight {
return ErrInvalidCommitHeight{
Expected: expected,
Actual: actual,
}
}
func (e ErrInvalidCommitHeight) Error() string {
return fmt.Sprintf("Invalid commit -- wrong height: %v vs %v", e.Expected, e.Actual)
}
func NewErrInvalidCommitSignatures(expected, actual int) ErrInvalidCommitSignatures {
return ErrInvalidCommitSignatures{
Expected: expected,
Actual: actual,
}
}
func (e ErrInvalidCommitSignatures) Error() string {
return fmt.Sprintf("Invalid commit -- wrong set size: %v vs %v", e.Expected, e.Actual)
}
+326
View File
@@ -0,0 +1,326 @@
package block
import (
"context"
"fmt"
"strings"
"github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
"github.com/tendermint/tendermint/libs/service"
)
const defaultCapacity = 0
type EventBusSubscriber interface {
Subscribe(ctx context.Context, subscriber string, query tmpubsub.Query, outCapacity ...int) (Subscription, error)
Unsubscribe(ctx context.Context, args tmpubsub.UnsubscribeArgs) error
UnsubscribeAll(ctx context.Context, subscriber string) error
NumClients() int
NumClientSubscriptions(clientID string) int
}
type Subscription interface {
ID() string
Out() <-chan tmpubsub.Message
Canceled() <-chan struct{}
Err() error
}
// EventBus is a common bus for all events going through the system. All calls
// are proxied to underlying pubsub server. All events must be published using
// EventBus to ensure correct data types.
type EventBus struct {
service.BaseService
pubsub *tmpubsub.Server
}
// NewEventBus returns a new event bus.
func NewEventBus() *EventBus {
return NewEventBusWithBufferCapacity(defaultCapacity)
}
// NewEventBusWithBufferCapacity returns a new event bus with the given buffer capacity.
func NewEventBusWithBufferCapacity(cap int) *EventBus {
// capacity could be exposed later if needed
pubsub := tmpubsub.NewServer(tmpubsub.BufferCapacity(cap))
b := &EventBus{pubsub: pubsub}
b.BaseService = *service.NewBaseService(nil, "EventBus", b)
return b
}
func (b *EventBus) SetLogger(l log.Logger) {
b.BaseService.SetLogger(l)
b.pubsub.SetLogger(l.With("module", "pubsub"))
}
func (b *EventBus) OnStart() error {
return b.pubsub.Start()
}
func (b *EventBus) OnStop() {
if err := b.pubsub.Stop(); err != nil {
b.pubsub.Logger.Error("error trying to stop eventBus", "error", err)
}
}
func (b *EventBus) NumClients() int {
return b.pubsub.NumClients()
}
func (b *EventBus) NumClientSubscriptions(clientID string) int {
return b.pubsub.NumClientSubscriptions(clientID)
}
func (b *EventBus) Subscribe(
ctx context.Context,
subscriber string,
query tmpubsub.Query,
outCapacity ...int,
) (Subscription, error) {
return b.pubsub.Subscribe(ctx, subscriber, query, outCapacity...)
}
// This method can be used for a local consensus explorer and synchronous
// testing. Do not use for for public facing / untrusted subscriptions!
func (b *EventBus) SubscribeUnbuffered(
ctx context.Context,
subscriber string,
query tmpubsub.Query,
) (Subscription, error) {
return b.pubsub.SubscribeUnbuffered(ctx, subscriber, query)
}
func (b *EventBus) Unsubscribe(ctx context.Context, args tmpubsub.UnsubscribeArgs) error {
return b.pubsub.Unsubscribe(ctx, args)
}
func (b *EventBus) UnsubscribeAll(ctx context.Context, subscriber string) error {
return b.pubsub.UnsubscribeAll(ctx, subscriber)
}
func (b *EventBus) Publish(eventValue string, eventData TMEventData) error {
// no explicit deadline for publishing events
ctx := context.Background()
tokens := strings.Split(EventTypeKey, ".")
event := types.Event{
Type: tokens[0],
Attributes: []types.EventAttribute{
{
Key: tokens[1],
Value: eventValue,
},
},
}
return b.pubsub.PublishWithEvents(ctx, eventData, []types.Event{event})
}
func (b *EventBus) PublishEventNewBlock(data EventDataNewBlock) error {
// no explicit deadline for publishing events
ctx := context.Background()
events := append(data.ResultBeginBlock.Events, data.ResultEndBlock.Events...)
// add Tendermint-reserved new block event
events = append(events, EventNewBlock)
return b.pubsub.PublishWithEvents(ctx, data, events)
}
func (b *EventBus) PublishEventNewBlockHeader(data EventDataNewBlockHeader) error {
// no explicit deadline for publishing events
ctx := context.Background()
events := append(data.ResultBeginBlock.Events, data.ResultEndBlock.Events...)
// add Tendermint-reserved new block header event
events = append(events, EventNewBlockHeader)
return b.pubsub.PublishWithEvents(ctx, data, events)
}
func (b *EventBus) PublishEventNewEvidence(evidence EventDataNewEvidence) error {
return b.Publish(EventNewEvidenceValue, evidence)
}
func (b *EventBus) PublishEventVote(data EventDataVote) error {
return b.Publish(EventVoteValue, data)
}
func (b *EventBus) PublishEventValidBlock(data EventDataRoundState) error {
return b.Publish(EventValidBlockValue, data)
}
func (b *EventBus) PublishEventBlockSyncStatus(data EventDataBlockSyncStatus) error {
return b.Publish(EventBlockSyncStatusValue, data)
}
func (b *EventBus) PublishEventStateSyncStatus(data EventDataStateSyncStatus) error {
return b.Publish(EventStateSyncStatusValue, data)
}
// PublishEventTx publishes tx event with events from Result. Note it will add
// predefined keys (EventTypeKey, TxHashKey). Existing events with the same keys
// will be overwritten.
func (b *EventBus) PublishEventTx(data EventDataTx) error {
// no explicit deadline for publishing events
ctx := context.Background()
events := data.Result.Events
// add Tendermint-reserved events
events = append(events, EventTx)
tokens := strings.Split(TxHashKey, ".")
events = append(events, types.Event{
Type: tokens[0],
Attributes: []types.EventAttribute{
{
Key: tokens[1],
Value: fmt.Sprintf("%X", Tx(data.Tx).Hash()),
},
},
})
tokens = strings.Split(TxHeightKey, ".")
events = append(events, types.Event{
Type: tokens[0],
Attributes: []types.EventAttribute{
{
Key: tokens[1],
Value: fmt.Sprintf("%d", data.Height),
},
},
})
return b.pubsub.PublishWithEvents(ctx, data, events)
}
func (b *EventBus) PublishEventNewRoundStep(data EventDataRoundState) error {
return b.Publish(EventNewRoundStepValue, data)
}
func (b *EventBus) PublishEventTimeoutPropose(data EventDataRoundState) error {
return b.Publish(EventTimeoutProposeValue, data)
}
func (b *EventBus) PublishEventTimeoutWait(data EventDataRoundState) error {
return b.Publish(EventTimeoutWaitValue, data)
}
func (b *EventBus) PublishEventNewRound(data EventDataNewRound) error {
return b.Publish(EventNewRoundValue, data)
}
func (b *EventBus) PublishEventCompleteProposal(data EventDataCompleteProposal) error {
return b.Publish(EventCompleteProposalValue, data)
}
func (b *EventBus) PublishEventPolka(data EventDataRoundState) error {
return b.Publish(EventPolkaValue, data)
}
func (b *EventBus) PublishEventUnlock(data EventDataRoundState) error {
return b.Publish(EventUnlockValue, data)
}
func (b *EventBus) PublishEventRelock(data EventDataRoundState) error {
return b.Publish(EventRelockValue, data)
}
func (b *EventBus) PublishEventLock(data EventDataRoundState) error {
return b.Publish(EventLockValue, data)
}
func (b *EventBus) PublishEventValidatorSetUpdates(data EventDataValidatorSetUpdates) error {
return b.Publish(EventValidatorSetUpdatesValue, data)
}
//-----------------------------------------------------------------------------
type NopEventBus struct{}
func (NopEventBus) Subscribe(
ctx context.Context,
subscriber string,
query tmpubsub.Query,
out chan<- interface{},
) error {
return nil
}
func (NopEventBus) Unsubscribe(ctx context.Context, args tmpubsub.UnsubscribeArgs) error {
return nil
}
func (NopEventBus) UnsubscribeAll(ctx context.Context, subscriber string) error {
return nil
}
func (NopEventBus) PublishEventNewBlock(data EventDataNewBlock) error {
return nil
}
func (NopEventBus) PublishEventNewBlockHeader(data EventDataNewBlockHeader) error {
return nil
}
func (NopEventBus) PublishEventNewEvidence(evidence EventDataNewEvidence) error {
return nil
}
func (NopEventBus) PublishEventVote(data EventDataVote) error {
return nil
}
func (NopEventBus) PublishEventTx(data EventDataTx) error {
return nil
}
func (NopEventBus) PublishEventNewRoundStep(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventTimeoutPropose(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventTimeoutWait(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventNewRound(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventCompleteProposal(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventPolka(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventUnlock(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventRelock(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventLock(data EventDataRoundState) error {
return nil
}
func (NopEventBus) PublishEventValidatorSetUpdates(data EventDataValidatorSetUpdates) error {
return nil
}
func (NopEventBus) PublishEventBlockSyncStatus(data EventDataBlockSyncStatus) error {
return nil
}
func (NopEventBus) PublishEventStateSyncStatus(data EventDataStateSyncStatus) error {
return nil
}
+511
View File
@@ -0,0 +1,511 @@
package block
import (
"context"
"fmt"
mrand "math/rand"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
)
func TestEventBusPublishEventTx(t *testing.T) {
eventBus := NewEventBus()
err := eventBus.Start()
require.NoError(t, err)
t.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
t.Error(err)
}
})
tx := Tx("foo")
result := abci.ResponseDeliverTx{
Data: []byte("bar"),
Events: []abci.Event{
{Type: "testType", Attributes: []abci.EventAttribute{{Key: "baz", Value: "1"}}},
},
}
// PublishEventTx adds 3 composite keys, so the query below should work
query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND testType.baz=1", tx.Hash())
txsSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
require.NoError(t, err)
done := make(chan struct{})
go func() {
msg := <-txsSub.Out()
edt := msg.Data().(EventDataTx)
assert.Equal(t, int64(1), edt.Height)
assert.Equal(t, uint32(0), edt.Index)
assert.EqualValues(t, tx, edt.Tx)
assert.Equal(t, result, edt.Result)
close(done)
}()
err = eventBus.PublishEventTx(EventDataTx{abci.TxResult{
Height: 1,
Index: 0,
Tx: tx,
Result: result,
}})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("did not receive a transaction after 1 sec.")
}
}
func TestEventBusPublishEventNewBlock(t *testing.T) {
eventBus := NewEventBus()
err := eventBus.Start()
require.NoError(t, err)
t.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
t.Error(err)
}
})
block := MakeBlock(0, []Tx{}, nil, []Evidence{})
blockID := BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(BlockPartSizeBytes).Header()}
resultBeginBlock := abci.ResponseBeginBlock{
Events: []abci.Event{
{Type: "testType", Attributes: []abci.EventAttribute{{Key: "baz", Value: "1"}}},
},
}
resultEndBlock := abci.ResponseEndBlock{
Events: []abci.Event{
{Type: "testType", Attributes: []abci.EventAttribute{{Key: "foz", Value: "2"}}},
},
}
// PublishEventNewBlock adds the tm.event compositeKey, so the query below should work
query := "tm.event='NewBlock' AND testType.baz=1 AND testType.foz=2"
blocksSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
require.NoError(t, err)
done := make(chan struct{})
go func() {
msg := <-blocksSub.Out()
edt := msg.Data().(EventDataNewBlock)
assert.Equal(t, block, edt.Block)
assert.Equal(t, blockID, edt.BlockID)
assert.Equal(t, resultBeginBlock, edt.ResultBeginBlock)
assert.Equal(t, resultEndBlock, edt.ResultEndBlock)
close(done)
}()
err = eventBus.PublishEventNewBlock(EventDataNewBlock{
Block: block,
BlockID: blockID,
ResultBeginBlock: resultBeginBlock,
ResultEndBlock: resultEndBlock,
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("did not receive a block after 1 sec.")
}
}
func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) {
eventBus := NewEventBus()
err := eventBus.Start()
require.NoError(t, err)
t.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
t.Error(err)
}
})
tx := Tx("foo")
result := abci.ResponseDeliverTx{
Data: []byte("bar"),
Events: []abci.Event{
{
Type: "transfer",
Attributes: []abci.EventAttribute{
{Key: "sender", Value: "foo"},
{Key: "recipient", Value: "bar"},
{Key: "amount", Value: "5"},
},
},
{
Type: "transfer",
Attributes: []abci.EventAttribute{
{Key: "sender", Value: "baz"},
{Key: "recipient", Value: "cat"},
{Key: "amount", Value: "13"},
},
},
{
Type: "withdraw.rewards",
Attributes: []abci.EventAttribute{
{Key: "address", Value: "bar"},
{Key: "source", Value: "iceman"},
{Key: "amount", Value: "33"},
},
},
},
}
testCases := []struct {
query string
expectResults bool
}{
{
"tm.event='Tx' AND tx.height=1 AND transfer.sender='DoesNotExist'",
false,
},
{
"tm.event='Tx' AND tx.height=1 AND transfer.sender='foo'",
true,
},
{
"tm.event='Tx' AND tx.height=1 AND transfer.sender='baz'",
true,
},
{
"tm.event='Tx' AND tx.height=1 AND transfer.sender='foo' AND transfer.sender='baz'",
true,
},
{
"tm.event='Tx' AND tx.height=1 AND transfer.sender='foo' AND transfer.sender='DoesNotExist'",
false,
},
}
for i, tc := range testCases {
sub, err := eventBus.Subscribe(context.Background(), fmt.Sprintf("client-%d", i), tmquery.MustParse(tc.query))
require.NoError(t, err)
done := make(chan struct{})
go func() {
select {
case msg := <-sub.Out():
data := msg.Data().(EventDataTx)
assert.Equal(t, int64(1), data.Height)
assert.Equal(t, uint32(0), data.Index)
assert.EqualValues(t, tx, data.Tx)
assert.Equal(t, result, data.Result)
close(done)
case <-time.After(1 * time.Second):
return
}
}()
err = eventBus.PublishEventTx(EventDataTx{abci.TxResult{
Height: 1,
Index: 0,
Tx: tx,
Result: result,
}})
assert.NoError(t, err)
select {
case <-done:
if !tc.expectResults {
require.Fail(t, "unexpected transaction result(s) from subscription")
}
case <-time.After(1 * time.Second):
if tc.expectResults {
require.Fail(t, "failed to receive a transaction after 1 second")
}
}
}
}
func TestEventBusPublishEventNewBlockHeader(t *testing.T) {
eventBus := NewEventBus()
err := eventBus.Start()
require.NoError(t, err)
t.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
t.Error(err)
}
})
block := MakeBlock(0, []Tx{}, nil, []Evidence{})
resultBeginBlock := abci.ResponseBeginBlock{
Events: []abci.Event{
{Type: "testType", Attributes: []abci.EventAttribute{{Key: "baz", Value: "1"}}},
},
}
resultEndBlock := abci.ResponseEndBlock{
Events: []abci.Event{
{Type: "testType", Attributes: []abci.EventAttribute{{Key: "foz", Value: "2"}}},
},
}
// PublishEventNewBlockHeader adds the tm.event compositeKey, so the query below should work
query := "tm.event='NewBlockHeader' AND testType.baz=1 AND testType.foz=2"
headersSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
require.NoError(t, err)
done := make(chan struct{})
go func() {
msg := <-headersSub.Out()
edt := msg.Data().(EventDataNewBlockHeader)
assert.Equal(t, block.Header, edt.Header)
assert.Equal(t, resultBeginBlock, edt.ResultBeginBlock)
assert.Equal(t, resultEndBlock, edt.ResultEndBlock)
close(done)
}()
err = eventBus.PublishEventNewBlockHeader(EventDataNewBlockHeader{
Header: block.Header,
ResultBeginBlock: resultBeginBlock,
ResultEndBlock: resultEndBlock,
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("did not receive a block header after 1 sec.")
}
}
func TestEventBusPublishEventNewEvidence(t *testing.T) {
eventBus := NewEventBus()
err := eventBus.Start()
require.NoError(t, err)
t.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
t.Error(err)
}
})
ev := NewMockDuplicateVoteEvidence(1, time.Now(), "test-chain-id")
query := "tm.event='NewEvidence'"
evSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
require.NoError(t, err)
done := make(chan struct{})
go func() {
msg := <-evSub.Out()
edt := msg.Data().(EventDataNewEvidence)
assert.Equal(t, ev, edt.Evidence)
assert.Equal(t, int64(4), edt.Height)
close(done)
}()
err = eventBus.PublishEventNewEvidence(EventDataNewEvidence{
Evidence: ev,
Height: 4,
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("did not receive a block header after 1 sec.")
}
}
func TestEventBusPublish(t *testing.T) {
eventBus := NewEventBus()
err := eventBus.Start()
require.NoError(t, err)
t.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
t.Error(err)
}
})
const numEventsExpected = 14
sub, err := eventBus.Subscribe(context.Background(), "test", tmquery.Empty{}, numEventsExpected)
require.NoError(t, err)
done := make(chan struct{})
go func() {
numEvents := 0
for range sub.Out() {
numEvents++
if numEvents >= numEventsExpected {
close(done)
return
}
}
}()
err = eventBus.Publish(EventNewBlockHeaderValue, EventDataNewBlockHeader{})
require.NoError(t, err)
err = eventBus.PublishEventNewBlock(EventDataNewBlock{})
require.NoError(t, err)
err = eventBus.PublishEventNewBlockHeader(EventDataNewBlockHeader{})
require.NoError(t, err)
err = eventBus.PublishEventVote(EventDataVote{})
require.NoError(t, err)
err = eventBus.PublishEventNewRoundStep(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventTimeoutPropose(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventTimeoutWait(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventNewRound(EventDataNewRound{})
require.NoError(t, err)
err = eventBus.PublishEventCompleteProposal(EventDataCompleteProposal{})
require.NoError(t, err)
err = eventBus.PublishEventPolka(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventUnlock(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventRelock(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventLock(EventDataRoundState{})
require.NoError(t, err)
err = eventBus.PublishEventValidatorSetUpdates(EventDataValidatorSetUpdates{})
require.NoError(t, err)
err = eventBus.PublishEventBlockSyncStatus(EventDataBlockSyncStatus{})
require.NoError(t, err)
err = eventBus.PublishEventStateSyncStatus(EventDataStateSyncStatus{})
require.NoError(t, err)
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatalf("expected to receive %d events after 1 sec.", numEventsExpected)
}
}
func BenchmarkEventBus(b *testing.B) {
benchmarks := []struct {
name string
numClients int
randQueries bool
randEvents bool
}{
{"10Clients1Query1Event", 10, false, false},
{"100Clients", 100, false, false},
{"1000Clients", 1000, false, false},
{"10ClientsRandQueries1Event", 10, true, false},
{"100Clients", 100, true, false},
{"1000Clients", 1000, true, false},
{"10ClientsRandQueriesRandEvents", 10, true, true},
{"100Clients", 100, true, true},
{"1000Clients", 1000, true, true},
{"10Clients1QueryRandEvents", 10, false, true},
{"100Clients", 100, false, true},
{"1000Clients", 1000, false, true},
}
for _, bm := range benchmarks {
bm := bm
b.Run(bm.name, func(b *testing.B) {
benchmarkEventBus(bm.numClients, bm.randQueries, bm.randEvents, b)
})
}
}
func benchmarkEventBus(numClients int, randQueries bool, randEvents bool, b *testing.B) {
// for random* functions
mrand.Seed(time.Now().Unix())
eventBus := NewEventBusWithBufferCapacity(0) // set buffer capacity to 0 so we are not testing cache
err := eventBus.Start()
if err != nil {
b.Error(err)
}
b.Cleanup(func() {
if err := eventBus.Stop(); err != nil {
b.Error(err)
}
})
ctx := context.Background()
q := EventQueryNewBlock
for i := 0; i < numClients; i++ {
if randQueries {
q = randQuery()
}
sub, err := eventBus.Subscribe(ctx, fmt.Sprintf("client-%d", i), q)
if err != nil {
b.Fatal(err)
}
go func() {
for {
select {
case <-sub.Out():
case <-sub.Canceled():
return
}
}
}()
}
eventValue := EventNewBlockValue
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if randEvents {
eventValue = randEventValue()
}
err := eventBus.Publish(eventValue, EventDataString("Gamora"))
if err != nil {
b.Error(err)
}
}
}
var events = []string{
EventNewBlockValue,
EventNewBlockHeaderValue,
EventNewRoundValue,
EventNewRoundStepValue,
EventTimeoutProposeValue,
EventCompleteProposalValue,
EventPolkaValue,
EventUnlockValue,
EventLockValue,
EventRelockValue,
EventTimeoutWaitValue,
EventVoteValue,
EventBlockSyncStatusValue,
EventStateSyncStatusValue,
}
func randEventValue() string {
return events[mrand.Intn(len(events))]
}
var queries = []tmpubsub.Query{
EventQueryNewBlock,
EventQueryNewBlockHeader,
EventQueryNewRound,
EventQueryNewRoundStep,
EventQueryTimeoutPropose,
EventQueryCompleteProposal,
EventQueryPolka,
EventQueryUnlock,
EventQueryLock,
EventQueryRelock,
EventQueryTimeoutWait,
EventQueryVote,
EventQueryBlockSyncStatus,
EventQueryStateSyncStatus,
}
func randQuery() tmpubsub.Query {
return queries[mrand.Intn(len(queries))]
}
+253
View File
@@ -0,0 +1,253 @@
package block
import (
"fmt"
"strings"
abci "github.com/tendermint/tendermint/abci/types"
tmjson "github.com/tendermint/tendermint/libs/json"
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
)
// Reserved event types (alphabetically sorted).
const (
// Block level events for mass consumption by users.
// These events are triggered from the state package,
// after a block has been committed.
// These are also used by the tx indexer for async indexing.
// All of this data can be fetched through the rpc.
EventNewBlockValue = "NewBlock"
EventNewBlockHeaderValue = "NewBlockHeader"
EventNewEvidenceValue = "NewEvidence"
EventTxValue = "Tx"
EventValidatorSetUpdatesValue = "ValidatorSetUpdates"
// Internal consensus events.
// These are used for testing the consensus state machine.
// They can also be used to build real-time consensus visualizers.
EventCompleteProposalValue = "CompleteProposal"
// The BlockSyncStatus event will be emitted when the node switching
// state sync mechanism between the consensus reactor and the blocksync reactor.
EventBlockSyncStatusValue = "BlockSyncStatus"
EventLockValue = "Lock"
EventNewRoundValue = "NewRound"
EventNewRoundStepValue = "NewRoundStep"
EventPolkaValue = "Polka"
EventRelockValue = "Relock"
EventStateSyncStatusValue = "StateSyncStatus"
EventTimeoutProposeValue = "TimeoutPropose"
EventTimeoutWaitValue = "TimeoutWait"
EventUnlockValue = "Unlock"
EventValidBlockValue = "ValidBlock"
EventVoteValue = "Vote"
)
// Pre-populated ABCI Tendermint-reserved events
var (
EventNewBlock = abci.Event{
Type: strings.Split(EventTypeKey, ".")[0],
Attributes: []abci.EventAttribute{
{
Key: strings.Split(EventTypeKey, ".")[1],
Value: EventNewBlockValue,
},
},
}
EventNewBlockHeader = abci.Event{
Type: strings.Split(EventTypeKey, ".")[0],
Attributes: []abci.EventAttribute{
{
Key: strings.Split(EventTypeKey, ".")[1],
Value: EventNewBlockHeaderValue,
},
},
}
EventNewEvidence = abci.Event{
Type: strings.Split(EventTypeKey, ".")[0],
Attributes: []abci.EventAttribute{
{
Key: strings.Split(EventTypeKey, ".")[1],
Value: EventNewEvidenceValue,
},
},
}
EventTx = abci.Event{
Type: strings.Split(EventTypeKey, ".")[0],
Attributes: []abci.EventAttribute{
{
Key: strings.Split(EventTypeKey, ".")[1],
Value: EventTxValue,
},
},
}
)
// ENCODING / DECODING
// TMEventData implements events.EventData.
type TMEventData interface {
// empty interface
}
func init() {
tmjson.RegisterType(EventDataNewBlock{}, "tendermint/event/NewBlock")
tmjson.RegisterType(EventDataNewBlockHeader{}, "tendermint/event/NewBlockHeader")
tmjson.RegisterType(EventDataNewEvidence{}, "tendermint/event/NewEvidence")
tmjson.RegisterType(EventDataTx{}, "tendermint/event/Tx")
tmjson.RegisterType(EventDataRoundState{}, "tendermint/event/RoundState")
tmjson.RegisterType(EventDataNewRound{}, "tendermint/event/NewRound")
tmjson.RegisterType(EventDataCompleteProposal{}, "tendermint/event/CompleteProposal")
tmjson.RegisterType(EventDataVote{}, "tendermint/event/Vote")
tmjson.RegisterType(EventDataValidatorSetUpdates{}, "tendermint/event/ValidatorSetUpdates")
tmjson.RegisterType(EventDataString(""), "tendermint/event/ProposalString")
tmjson.RegisterType(EventDataBlockSyncStatus{}, "tendermint/event/FastSyncStatus")
tmjson.RegisterType(EventDataStateSyncStatus{}, "tendermint/event/StateSyncStatus")
}
// Most event messages are basic types (a block, a transaction)
// but some (an input to a call tx or a receive) are more exotic
type EventDataNewBlock struct {
Block *Block `json:"block"`
BlockID BlockID `json:"block_id"`
ResultBeginBlock abci.ResponseBeginBlock `json:"result_begin_block"`
ResultEndBlock abci.ResponseEndBlock `json:"result_end_block"`
}
type EventDataNewBlockHeader struct {
Header Header `json:"header"`
NumTxs int64 `json:"num_txs"` // Number of txs in a block
ResultBeginBlock abci.ResponseBeginBlock `json:"result_begin_block"`
ResultEndBlock abci.ResponseEndBlock `json:"result_end_block"`
}
type EventDataNewEvidence struct {
Evidence Evidence `json:"evidence"`
Height int64 `json:"height"`
}
// All txs fire EventDataTx
type EventDataTx struct {
abci.TxResult
}
// NOTE: This goes into the replay WAL
type EventDataRoundState struct {
Height int64 `json:"height"`
Round int32 `json:"round"`
Step string `json:"step"`
}
type ValidatorInfo struct {
Address Address `json:"address"`
Index int32 `json:"index"`
}
type EventDataNewRound struct {
Height int64 `json:"height"`
Round int32 `json:"round"`
Step string `json:"step"`
Proposer ValidatorInfo `json:"proposer"`
}
type EventDataCompleteProposal struct {
Height int64 `json:"height"`
Round int32 `json:"round"`
Step string `json:"step"`
BlockID BlockID `json:"block_id"`
}
type EventDataVote struct {
Vote *Vote
}
type EventDataString string
type EventDataValidatorSetUpdates struct {
ValidatorUpdates []*Validator `json:"validator_updates"`
}
// EventDataBlockSyncStatus shows the fastsync status and the
// height when the node state sync mechanism changes.
type EventDataBlockSyncStatus struct {
Complete bool `json:"complete"`
Height int64 `json:"height"`
}
// EventDataStateSyncStatus shows the statesync status and the
// height when the node state sync mechanism changes.
type EventDataStateSyncStatus struct {
Complete bool `json:"complete"`
Height int64 `json:"height"`
}
// PUBSUB
const (
// EventTypeKey is a reserved composite key for event name.
EventTypeKey = "tm.event"
// TxHashKey is a reserved key, used to specify transaction's hash.
// see EventBus#PublishEventTx
TxHashKey = "tx.hash"
// TxHeightKey is a reserved key, used to specify transaction block's height.
// see EventBus#PublishEventTx
TxHeightKey = "tx.height"
// BlockHeightKey is a reserved key used for indexing BeginBlock and Endblock
// events.
BlockHeightKey = "block.height"
EventTypeBeginBlock = "begin_block"
EventTypeEndBlock = "end_block"
)
var (
EventQueryCompleteProposal = QueryForEvent(EventCompleteProposalValue)
EventQueryLock = QueryForEvent(EventLockValue)
EventQueryNewBlock = QueryForEvent(EventNewBlockValue)
EventQueryNewBlockHeader = QueryForEvent(EventNewBlockHeaderValue)
EventQueryNewEvidence = QueryForEvent(EventNewEvidenceValue)
EventQueryNewRound = QueryForEvent(EventNewRoundValue)
EventQueryNewRoundStep = QueryForEvent(EventNewRoundStepValue)
EventQueryPolka = QueryForEvent(EventPolkaValue)
EventQueryRelock = QueryForEvent(EventRelockValue)
EventQueryTimeoutPropose = QueryForEvent(EventTimeoutProposeValue)
EventQueryTimeoutWait = QueryForEvent(EventTimeoutWaitValue)
EventQueryTx = QueryForEvent(EventTxValue)
EventQueryUnlock = QueryForEvent(EventUnlockValue)
EventQueryValidatorSetUpdates = QueryForEvent(EventValidatorSetUpdatesValue)
EventQueryValidBlock = QueryForEvent(EventValidBlockValue)
EventQueryVote = QueryForEvent(EventVoteValue)
EventQueryBlockSyncStatus = QueryForEvent(EventBlockSyncStatusValue)
EventQueryStateSyncStatus = QueryForEvent(EventStateSyncStatusValue)
)
func EventQueryTxFor(tx Tx) tmpubsub.Query {
return tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s='%X'", EventTypeKey, EventTxValue, TxHashKey, tx.Hash()))
}
func QueryForEvent(eventValue string) tmpubsub.Query {
return tmquery.MustParse(fmt.Sprintf("%s='%s'", EventTypeKey, eventValue))
}
// BlockEventPublisher publishes all block related events
type BlockEventPublisher interface {
PublishEventNewBlock(block EventDataNewBlock) error
PublishEventNewBlockHeader(header EventDataNewBlockHeader) error
PublishEventNewEvidence(evidence EventDataNewEvidence) error
PublishEventTx(EventDataTx) error
PublishEventValidatorSetUpdates(EventDataValidatorSetUpdates) error
}
type TxEventPublisher interface {
PublishEventTx(EventDataTx) error
}
+27
View File
@@ -0,0 +1,27 @@
package block
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestQueryTxFor(t *testing.T) {
tx := Tx("foo")
assert.Equal(t,
fmt.Sprintf("tm.event='Tx' AND tx.hash='%X'", tx.Hash()),
EventQueryTxFor(tx).String(),
)
}
func TestQueryForEvent(t *testing.T) {
assert.Equal(t,
"tm.event='NewBlock'",
QueryForEvent(EventNewBlockValue).String(),
)
assert.Equal(t,
"tm.event='NewEvidence'",
QueryForEvent(EventNewEvidenceValue).String(),
)
}
+711
View File
@@ -0,0 +1,711 @@
package block
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"sort"
"strings"
"time"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/crypto/tmhash"
tmjson "github.com/tendermint/tendermint/libs/json"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// Evidence represents any provable malicious activity by a validator.
// Verification logic for each evidence is part of the evidence module.
type Evidence interface {
ABCI() []abci.Evidence // forms individual evidence to be sent to the application
Bytes() []byte // bytes which comprise the evidence
Hash() []byte // hash of the evidence
Height() int64 // height of the infraction
String() string // string format of the evidence
Time() time.Time // time of the infraction
ValidateBasic() error // basic consistency check
}
//--------------------------------------------------------------------------------------
// DuplicateVoteEvidence contains evidence of a single validator signing two conflicting votes.
type DuplicateVoteEvidence struct {
VoteA *Vote `json:"vote_a"`
VoteB *Vote `json:"vote_b"`
// abci specific information
TotalVotingPower int64
ValidatorPower int64
Timestamp time.Time
}
var _ Evidence = &DuplicateVoteEvidence{}
// NewDuplicateVoteEvidence creates DuplicateVoteEvidence with right ordering given
// two conflicting votes. If one of the votes is nil, evidence returned is nil as well
func NewDuplicateVoteEvidence(vote1, vote2 *Vote, blockTime time.Time, valSet *ValidatorSet) *DuplicateVoteEvidence {
var voteA, voteB *Vote
if vote1 == nil || vote2 == nil || valSet == nil {
return nil
}
idx, val := valSet.GetByAddress(vote1.ValidatorAddress)
if idx == -1 {
return nil
}
if strings.Compare(vote1.BlockID.Key(), vote2.BlockID.Key()) == -1 {
voteA = vote1
voteB = vote2
} else {
voteA = vote2
voteB = vote1
}
return &DuplicateVoteEvidence{
VoteA: voteA,
VoteB: voteB,
TotalVotingPower: valSet.TotalVotingPower(),
ValidatorPower: val.VotingPower,
Timestamp: blockTime,
}
}
// ABCI returns the application relevant representation of the evidence
func (dve *DuplicateVoteEvidence) ABCI() []abci.Evidence {
return []abci.Evidence{{
Type: abci.EvidenceType_DUPLICATE_VOTE,
Validator: abci.Validator{
Address: dve.VoteA.ValidatorAddress,
Power: dve.ValidatorPower,
},
Height: dve.VoteA.Height,
Time: dve.Timestamp,
TotalVotingPower: dve.TotalVotingPower,
}}
}
// Bytes returns the proto-encoded evidence as a byte array.
func (dve *DuplicateVoteEvidence) Bytes() []byte {
pbe := dve.ToProto()
bz, err := pbe.Marshal()
if err != nil {
panic(err)
}
return bz
}
// Hash returns the hash of the evidence.
func (dve *DuplicateVoteEvidence) Hash() []byte {
return tmhash.Sum(dve.Bytes())
}
// Height returns the height of the infraction
func (dve *DuplicateVoteEvidence) Height() int64 {
return dve.VoteA.Height
}
// String returns a string representation of the evidence.
func (dve *DuplicateVoteEvidence) String() string {
return fmt.Sprintf("DuplicateVoteEvidence{VoteA: %v, VoteB: %v}", dve.VoteA, dve.VoteB)
}
// Time returns the time of the infraction
func (dve *DuplicateVoteEvidence) Time() time.Time {
return dve.Timestamp
}
// ValidateBasic performs basic validation.
func (dve *DuplicateVoteEvidence) ValidateBasic() error {
if dve == nil {
return errors.New("empty duplicate vote evidence")
}
if dve.VoteA == nil || dve.VoteB == nil {
return fmt.Errorf("one or both of the votes are empty %v, %v", dve.VoteA, dve.VoteB)
}
if err := dve.VoteA.ValidateBasic(); err != nil {
return fmt.Errorf("invalid VoteA: %w", err)
}
if err := dve.VoteB.ValidateBasic(); err != nil {
return fmt.Errorf("invalid VoteB: %w", err)
}
// Enforce Votes are lexicographically sorted on blockID
if strings.Compare(dve.VoteA.BlockID.Key(), dve.VoteB.BlockID.Key()) >= 0 {
return errors.New("duplicate votes in invalid order")
}
return nil
}
// ValidateABCI validates the ABCI component of the evidence by checking the
// timestamp, validator power and total voting power.
func (dve *DuplicateVoteEvidence) ValidateABCI(
val *Validator,
valSet *ValidatorSet,
evidenceTime time.Time,
) error {
if dve.Timestamp != evidenceTime {
return fmt.Errorf(
"evidence has a different time to the block it is associated with (%v != %v)",
dve.Timestamp, evidenceTime)
}
if val.VotingPower != dve.ValidatorPower {
return fmt.Errorf("validator power from evidence and our validator set does not match (%d != %d)",
dve.ValidatorPower, val.VotingPower)
}
if valSet.TotalVotingPower() != dve.TotalVotingPower {
return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)",
dve.TotalVotingPower, valSet.TotalVotingPower())
}
return nil
}
// GenerateABCI populates the ABCI component of the evidence. This includes the
// validator power, timestamp and total voting power.
func (dve *DuplicateVoteEvidence) GenerateABCI(
val *Validator,
valSet *ValidatorSet,
evidenceTime time.Time,
) {
dve.ValidatorPower = val.VotingPower
dve.TotalVotingPower = valSet.TotalVotingPower()
dve.Timestamp = evidenceTime
}
// ToProto encodes DuplicateVoteEvidence to protobuf
func (dve *DuplicateVoteEvidence) ToProto() *tmproto.DuplicateVoteEvidence {
voteB := dve.VoteB.ToProto()
voteA := dve.VoteA.ToProto()
tp := tmproto.DuplicateVoteEvidence{
VoteA: voteA,
VoteB: voteB,
TotalVotingPower: dve.TotalVotingPower,
ValidatorPower: dve.ValidatorPower,
Timestamp: dve.Timestamp,
}
return &tp
}
// DuplicateVoteEvidenceFromProto decodes protobuf into DuplicateVoteEvidence
func DuplicateVoteEvidenceFromProto(pb *tmproto.DuplicateVoteEvidence) (*DuplicateVoteEvidence, error) {
if pb == nil {
return nil, errors.New("nil duplicate vote evidence")
}
vA, err := VoteFromProto(pb.VoteA)
if err != nil {
return nil, err
}
vB, err := VoteFromProto(pb.VoteB)
if err != nil {
return nil, err
}
dve := &DuplicateVoteEvidence{
VoteA: vA,
VoteB: vB,
TotalVotingPower: pb.TotalVotingPower,
ValidatorPower: pb.ValidatorPower,
Timestamp: pb.Timestamp,
}
return dve, dve.ValidateBasic()
}
//------------------------------------ LIGHT EVIDENCE --------------------------------------
// LightClientAttackEvidence is a generalized evidence that captures all forms of known attacks on
// a light client such that a full node can verify, propose and commit the evidence on-chain for
// punishment of the malicious validators. There are three forms of attacks: Lunatic, Equivocation
// and Amnesia. These attacks are exhaustive. You can find a more detailed overview of this at
// tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md
//
// CommonHeight is used to indicate the type of attack. If the height is different to the conflicting block
// height, then nodes will treat this as of the Lunatic form, else it is of the Equivocation form.
type LightClientAttackEvidence struct {
ConflictingBlock *LightBlock
CommonHeight int64
// abci specific information
ByzantineValidators []*Validator // validators in the validator set that misbehaved in creating the conflicting block
TotalVotingPower int64 // total voting power of the validator set at the common height
Timestamp time.Time // timestamp of the block at the common height
}
var _ Evidence = &LightClientAttackEvidence{}
// ABCI forms an array of abci evidence for each byzantine validator
func (l *LightClientAttackEvidence) ABCI() []abci.Evidence {
abciEv := make([]abci.Evidence, len(l.ByzantineValidators))
for idx, val := range l.ByzantineValidators {
abciEv[idx] = abci.Evidence{
Type: abci.EvidenceType_LIGHT_CLIENT_ATTACK,
Validator: TM2PB.Validator(val),
Height: l.Height(),
Time: l.Timestamp,
TotalVotingPower: l.TotalVotingPower,
}
}
return abciEv
}
// Bytes returns the proto-encoded evidence as a byte array
func (l *LightClientAttackEvidence) Bytes() []byte {
pbe, err := l.ToProto()
if err != nil {
panic(err)
}
bz, err := pbe.Marshal()
if err != nil {
panic(err)
}
return bz
}
// GetByzantineValidators finds out what style of attack LightClientAttackEvidence was and then works out who
// the malicious validators were and returns them. This is used both for forming the ByzantineValidators
// field and for validating that it is correct. Validators are ordered based on validator power
func (l *LightClientAttackEvidence) GetByzantineValidators(commonVals *ValidatorSet,
trusted *SignedHeader) []*Validator {
var validators []*Validator
// First check if the header is invalid. This means that it is a lunatic attack and therefore we take the
// validators who are in the commonVals and voted for the lunatic header
if l.ConflictingHeaderIsInvalid(trusted.Header) {
for _, commitSig := range l.ConflictingBlock.Commit.Signatures {
if !commitSig.ForBlock() {
continue
}
_, val := commonVals.GetByAddress(commitSig.ValidatorAddress)
if val == nil {
// validator wasn't in the common validator set
continue
}
validators = append(validators, val)
}
sort.Sort(ValidatorsByVotingPower(validators))
return validators
} else if trusted.Commit.Round == l.ConflictingBlock.Commit.Round {
// This is an equivocation attack as both commits are in the same round. We then find the validators
// from the conflicting light block validator set that voted in both headers.
// Validator hashes are the same therefore the indexing order of validators are the same and thus we
// only need a single loop to find the validators that voted twice.
for i := 0; i < len(l.ConflictingBlock.Commit.Signatures); i++ {
sigA := l.ConflictingBlock.Commit.Signatures[i]
if !sigA.ForBlock() {
continue
}
sigB := trusted.Commit.Signatures[i]
if !sigB.ForBlock() {
continue
}
_, val := l.ConflictingBlock.ValidatorSet.GetByAddress(sigA.ValidatorAddress)
validators = append(validators, val)
}
sort.Sort(ValidatorsByVotingPower(validators))
return validators
}
// if the rounds are different then this is an amnesia attack. Unfortunately, given the nature of the attack,
// we aren't able yet to deduce which are malicious validators and which are not hence we return an
// empty validator set.
return validators
}
// ConflictingHeaderIsInvalid takes a trusted header and matches it againt a conflicting header
// to determine whether the conflicting header was the product of a valid state transition
// or not. If it is then all the deterministic fields of the header should be the same.
// If not, it is an invalid header and constitutes a lunatic attack.
func (l *LightClientAttackEvidence) ConflictingHeaderIsInvalid(trustedHeader *Header) bool {
return !bytes.Equal(trustedHeader.ValidatorsHash, l.ConflictingBlock.ValidatorsHash) ||
!bytes.Equal(trustedHeader.NextValidatorsHash, l.ConflictingBlock.NextValidatorsHash) ||
!bytes.Equal(trustedHeader.ConsensusHash, l.ConflictingBlock.ConsensusHash) ||
!bytes.Equal(trustedHeader.AppHash, l.ConflictingBlock.AppHash) ||
!bytes.Equal(trustedHeader.LastResultsHash, l.ConflictingBlock.LastResultsHash)
}
// Hash returns the hash of the header and the commonHeight. This is designed to cause hash collisions
// with evidence that have the same conflicting header and common height but different permutations
// of validator commit signatures. The reason for this is that we don't want to allow several
// permutations of the same evidence to be committed on chain. Ideally we commit the header with the
// most commit signatures (captures the most byzantine validators) but anything greater than 1/3 is
// sufficient.
// TODO: We should change the hash to include the commit, header, total voting power, byzantine
// validators and timestamp
func (l *LightClientAttackEvidence) Hash() []byte {
buf := make([]byte, binary.MaxVarintLen64)
n := binary.PutVarint(buf, l.CommonHeight)
bz := make([]byte, tmhash.Size+n)
copy(bz[:tmhash.Size-1], l.ConflictingBlock.Hash().Bytes())
copy(bz[tmhash.Size:], buf)
return tmhash.Sum(bz)
}
// Height returns the last height at which the primary provider and witness provider had the same header.
// We use this as the height of the infraction rather than the actual conflicting header because we know
// that the malicious validators were bonded at this height which is important for evidence expiry
func (l *LightClientAttackEvidence) Height() int64 {
return l.CommonHeight
}
// String returns a string representation of LightClientAttackEvidence
func (l *LightClientAttackEvidence) String() string {
return fmt.Sprintf(`LightClientAttackEvidence{
ConflictingBlock: %v,
CommonHeight: %d,
ByzatineValidators: %v,
TotalVotingPower: %d,
Timestamp: %v}#%X`,
l.ConflictingBlock.String(), l.CommonHeight, l.ByzantineValidators,
l.TotalVotingPower, l.Timestamp, l.Hash())
}
// Time returns the time of the common block where the infraction leveraged off.
func (l *LightClientAttackEvidence) Time() time.Time {
return l.Timestamp
}
// ValidateBasic performs basic validation such that the evidence is consistent and can now be used for verification.
func (l *LightClientAttackEvidence) ValidateBasic() error {
if l.ConflictingBlock == nil {
return errors.New("conflicting block is nil")
}
// this check needs to be done before we can run validate basic
if l.ConflictingBlock.Header == nil {
return errors.New("conflicting block missing header")
}
if l.TotalVotingPower <= 0 {
return errors.New("negative or zero total voting power")
}
if l.CommonHeight <= 0 {
return errors.New("negative or zero common height")
}
// check that common height isn't ahead of the height of the conflicting block. It
// is possible that they are the same height if the light node witnesses either an
// amnesia or a equivocation attack.
if l.CommonHeight > l.ConflictingBlock.Height {
return fmt.Errorf("common height is ahead of the conflicting block height (%d > %d)",
l.CommonHeight, l.ConflictingBlock.Height)
}
if err := l.ConflictingBlock.ValidateBasic(l.ConflictingBlock.ChainID); err != nil {
return fmt.Errorf("invalid conflicting light block: %w", err)
}
return nil
}
// ValidateABCI validates the ABCI component of the evidence by checking the
// timestamp, byzantine validators and total voting power all match. ABCI
// components are validated separately because they can be re generated if
// invalid.
func (l *LightClientAttackEvidence) ValidateABCI(
commonVals *ValidatorSet,
trustedHeader *SignedHeader,
evidenceTime time.Time,
) error {
if evTotal, valsTotal := l.TotalVotingPower, commonVals.TotalVotingPower(); evTotal != valsTotal {
return fmt.Errorf("total voting power from the evidence and our validator set does not match (%d != %d)",
evTotal, valsTotal)
}
if l.Timestamp != evidenceTime {
return fmt.Errorf(
"evidence has a different time to the block it is associated with (%v != %v)",
l.Timestamp, evidenceTime)
}
// Find out what type of attack this was and thus extract the malicious
// validators. Note, in the case of an Amnesia attack we don't have any
// malicious validators.
validators := l.GetByzantineValidators(commonVals, trustedHeader)
// Ensure this matches the validators that are listed in the evidence. They
// should be ordered based on power.
if validators == nil && l.ByzantineValidators != nil {
return fmt.Errorf(
"expected nil validators from an amnesia light client attack but got %d",
len(l.ByzantineValidators),
)
}
if exp, got := len(validators), len(l.ByzantineValidators); exp != got {
return fmt.Errorf("expected %d byzantine validators from evidence but got %d", exp, got)
}
for idx, val := range validators {
if !bytes.Equal(l.ByzantineValidators[idx].Address, val.Address) {
return fmt.Errorf(
"evidence contained an unexpected byzantine validator address; expected: %v, got: %v",
val.Address, l.ByzantineValidators[idx].Address,
)
}
if l.ByzantineValidators[idx].VotingPower != val.VotingPower {
return fmt.Errorf(
"evidence contained unexpected byzantine validator power; expected %d, got %d",
val.VotingPower, l.ByzantineValidators[idx].VotingPower,
)
}
}
return nil
}
// GenerateABCI populates the ABCI component of the evidence: the timestamp,
// total voting power and byantine validators
func (l *LightClientAttackEvidence) GenerateABCI(
commonVals *ValidatorSet,
trustedHeader *SignedHeader,
evidenceTime time.Time,
) {
l.Timestamp = evidenceTime
l.TotalVotingPower = commonVals.TotalVotingPower()
l.ByzantineValidators = l.GetByzantineValidators(commonVals, trustedHeader)
}
// ToProto encodes LightClientAttackEvidence to protobuf
func (l *LightClientAttackEvidence) ToProto() (*tmproto.LightClientAttackEvidence, error) {
conflictingBlock, err := l.ConflictingBlock.ToProto()
if err != nil {
return nil, err
}
byzVals := make([]*tmproto.Validator, len(l.ByzantineValidators))
for idx, val := range l.ByzantineValidators {
valpb, err := val.ToProto()
if err != nil {
return nil, err
}
byzVals[idx] = valpb
}
return &tmproto.LightClientAttackEvidence{
ConflictingBlock: conflictingBlock,
CommonHeight: l.CommonHeight,
ByzantineValidators: byzVals,
TotalVotingPower: l.TotalVotingPower,
Timestamp: l.Timestamp,
}, nil
}
// LightClientAttackEvidenceFromProto decodes protobuf
func LightClientAttackEvidenceFromProto(lpb *tmproto.LightClientAttackEvidence) (*LightClientAttackEvidence, error) {
if lpb == nil {
return nil, errors.New("empty light client attack evidence")
}
conflictingBlock, err := LightBlockFromProto(lpb.ConflictingBlock)
if err != nil {
return nil, err
}
byzVals := make([]*Validator, len(lpb.ByzantineValidators))
for idx, valpb := range lpb.ByzantineValidators {
val, err := ValidatorFromProto(valpb)
if err != nil {
return nil, err
}
byzVals[idx] = val
}
l := &LightClientAttackEvidence{
ConflictingBlock: conflictingBlock,
CommonHeight: lpb.CommonHeight,
ByzantineValidators: byzVals,
TotalVotingPower: lpb.TotalVotingPower,
Timestamp: lpb.Timestamp,
}
return l, l.ValidateBasic()
}
//------------------------------------------------------------------------------------------
// EvidenceList is a list of Evidence. Evidences is not a word.
type EvidenceList []Evidence
// Hash returns the simple merkle root hash of the EvidenceList.
func (evl EvidenceList) Hash() []byte {
// These allocations are required because Evidence is not of type Bytes, and
// golang slices can't be typed cast. This shouldn't be a performance problem since
// the Evidence size is capped.
evidenceBzs := make([][]byte, len(evl))
for i := 0; i < len(evl); i++ {
// TODO: We should change this to the hash. Using bytes contains some unexported data that
// may cause different hashes
evidenceBzs[i] = evl[i].Bytes()
}
return merkle.HashFromByteSlices(evidenceBzs)
}
func (evl EvidenceList) String() string {
s := ""
for _, e := range evl {
s += fmt.Sprintf("%s\t\t", e)
}
return s
}
// Has returns true if the evidence is in the EvidenceList.
func (evl EvidenceList) Has(evidence Evidence) bool {
for _, ev := range evl {
if bytes.Equal(evidence.Hash(), ev.Hash()) {
return true
}
}
return false
}
//------------------------------------------ PROTO --------------------------------------
// EvidenceToProto is a generalized function for encoding evidence that conforms to the
// evidence interface to protobuf
func EvidenceToProto(evidence Evidence) (*tmproto.Evidence, error) {
if evidence == nil {
return nil, errors.New("nil evidence")
}
switch evi := evidence.(type) {
case *DuplicateVoteEvidence:
pbev := evi.ToProto()
return &tmproto.Evidence{
Sum: &tmproto.Evidence_DuplicateVoteEvidence{
DuplicateVoteEvidence: pbev,
},
}, nil
case *LightClientAttackEvidence:
pbev, err := evi.ToProto()
if err != nil {
return nil, err
}
return &tmproto.Evidence{
Sum: &tmproto.Evidence_LightClientAttackEvidence{
LightClientAttackEvidence: pbev,
},
}, nil
default:
return nil, fmt.Errorf("toproto: evidence is not recognized: %T", evi)
}
}
// EvidenceFromProto is a generalized function for decoding protobuf into the
// evidence interface
func EvidenceFromProto(evidence *tmproto.Evidence) (Evidence, error) {
if evidence == nil {
return nil, errors.New("nil evidence")
}
switch evi := evidence.Sum.(type) {
case *tmproto.Evidence_DuplicateVoteEvidence:
return DuplicateVoteEvidenceFromProto(evi.DuplicateVoteEvidence)
case *tmproto.Evidence_LightClientAttackEvidence:
return LightClientAttackEvidenceFromProto(evi.LightClientAttackEvidence)
default:
return nil, errors.New("evidence is not recognized")
}
}
func init() {
tmjson.RegisterType(&DuplicateVoteEvidence{}, "tendermint/DuplicateVoteEvidence")
tmjson.RegisterType(&LightClientAttackEvidence{}, "tendermint/LightClientAttackEvidence")
}
//-------------------------------------------- ERRORS --------------------------------------
// ErrInvalidEvidence wraps a piece of evidence and the error denoting how or why it is invalid.
type ErrInvalidEvidence struct {
Evidence Evidence
Reason error
}
// NewErrInvalidEvidence returns a new EvidenceInvalid with the given err.
func NewErrInvalidEvidence(ev Evidence, err error) *ErrInvalidEvidence {
return &ErrInvalidEvidence{ev, err}
}
// Error returns a string representation of the error.
func (err *ErrInvalidEvidence) Error() string {
return fmt.Sprintf("Invalid evidence: %v. Evidence: %v", err.Reason, err.Evidence)
}
// ErrEvidenceOverflow is for when there the amount of evidence exceeds the max bytes.
type ErrEvidenceOverflow struct {
Max int64
Got int64
}
// NewErrEvidenceOverflow returns a new ErrEvidenceOverflow where got > max.
func NewErrEvidenceOverflow(max, got int64) *ErrEvidenceOverflow {
return &ErrEvidenceOverflow{max, got}
}
// Error returns a string representation of the error.
func (err *ErrEvidenceOverflow) Error() string {
return fmt.Sprintf("Too much evidence: Max %d, got %d", err.Max, err.Got)
}
//-------------------------------------------- MOCKING --------------------------------------
// unstable - use only for testing
// assumes the round to be 0 and the validator index to be 0
func NewMockDuplicateVoteEvidence(height int64, time time.Time, chainID string) *DuplicateVoteEvidence {
val := NewMockPV()
return NewMockDuplicateVoteEvidenceWithValidator(height, time, val, chainID)
}
// assumes voting power to be 10 and validator to be the only one in the set
func NewMockDuplicateVoteEvidenceWithValidator(height int64, time time.Time,
pv PrivValidator, chainID string) *DuplicateVoteEvidence {
pubKey, _ := pv.GetPubKey(context.Background())
val := NewValidator(pubKey, 10)
voteA := makeMockVote(height, 0, 0, pubKey.Address(), randBlockID(), time)
vA := voteA.ToProto()
_ = pv.SignVote(context.Background(), chainID, vA)
voteA.Signature = vA.Signature
voteB := makeMockVote(height, 0, 0, pubKey.Address(), randBlockID(), time)
vB := voteB.ToProto()
_ = pv.SignVote(context.Background(), chainID, vB)
voteB.Signature = vB.Signature
return NewDuplicateVoteEvidence(voteA, voteB, time, NewValidatorSet([]*Validator{val}))
}
func makeMockVote(height int64, round, index int32, addr Address,
blockID BlockID, time time.Time) *Vote {
return &Vote{
Type: tmproto.SignedMsgType(2),
Height: height,
Round: round,
BlockID: blockID,
Timestamp: time,
ValidatorAddress: addr,
ValidatorIndex: index,
}
}
func randBlockID() BlockID {
return BlockID{
Hash: tmrand.Bytes(tmhash.Size),
PartSetHeader: PartSetHeader{
Total: 1,
Hash: tmrand.Bytes(tmhash.Size),
},
}
}
+393
View File
@@ -0,0 +1,393 @@
package block
import (
"context"
"encoding/hex"
"math"
mrand "math/rand"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/tmhash"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/version"
)
var defaultVoteTime = time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
func TestEvidenceList(t *testing.T) {
ev := randomDuplicateVoteEvidence(t)
evl := EvidenceList([]Evidence{ev})
assert.NotNil(t, evl.Hash())
assert.True(t, evl.Has(ev))
assert.False(t, evl.Has(&DuplicateVoteEvidence{}))
}
func randomDuplicateVoteEvidence(t *testing.T) *DuplicateVoteEvidence {
val := NewMockPV()
blockID := makeBlockID([]byte("blockhash"), 1000, []byte("partshash"))
blockID2 := makeBlockID([]byte("blockhash2"), 1000, []byte("partshash"))
const chainID = "mychain"
return &DuplicateVoteEvidence{
VoteA: makeVote(t, val, chainID, 0, 10, 2, 1, blockID, defaultVoteTime),
VoteB: makeVote(t, val, chainID, 0, 10, 2, 1, blockID2, defaultVoteTime.Add(1*time.Minute)),
TotalVotingPower: 30,
ValidatorPower: 10,
Timestamp: defaultVoteTime,
}
}
func TestDuplicateVoteEvidence(t *testing.T) {
const height = int64(13)
ev := NewMockDuplicateVoteEvidence(height, time.Now(), "mock-chain-id")
assert.Equal(t, ev.Hash(), tmhash.Sum(ev.Bytes()))
assert.NotNil(t, ev.String())
assert.Equal(t, ev.Height(), height)
}
func TestDuplicateVoteEvidenceValidation(t *testing.T) {
val := NewMockPV()
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
blockID2 := makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
const chainID = "mychain"
testCases := []struct {
testName string
malleateEvidence func(*DuplicateVoteEvidence)
expectErr bool
}{
{"Good DuplicateVoteEvidence", func(ev *DuplicateVoteEvidence) {}, false},
{"Nil vote A", func(ev *DuplicateVoteEvidence) { ev.VoteA = nil }, true},
{"Nil vote B", func(ev *DuplicateVoteEvidence) { ev.VoteB = nil }, true},
{"Nil votes", func(ev *DuplicateVoteEvidence) {
ev.VoteA = nil
ev.VoteB = nil
}, true},
{"Invalid vote type", func(ev *DuplicateVoteEvidence) {
ev.VoteA = makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, math.MaxInt32, 0, blockID2, defaultVoteTime)
}, true},
{"Invalid vote order", func(ev *DuplicateVoteEvidence) {
swap := ev.VoteA.Copy()
ev.VoteA = ev.VoteB.Copy()
ev.VoteB = swap
}, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
vote1 := makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, math.MaxInt32, 0x02, blockID, defaultVoteTime)
vote2 := makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, math.MaxInt32, 0x02, blockID2, defaultVoteTime)
valSet := NewValidatorSet([]*Validator{val.ExtractIntoValidator(10)})
ev := NewDuplicateVoteEvidence(vote1, vote2, defaultVoteTime, valSet)
tc.malleateEvidence(ev)
assert.Equal(t, tc.expectErr, ev.ValidateBasic() != nil, "Validate Basic had an unexpected result")
})
}
}
func TestLightClientAttackEvidenceBasic(t *testing.T) {
height := int64(5)
commonHeight := height - 1
nValidators := 10
voteSet, valSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, nValidators, 1)
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)
require.NoError(t, err)
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
},
ValidatorSet: valSet,
},
CommonHeight: commonHeight,
TotalVotingPower: valSet.TotalVotingPower(),
Timestamp: header.Time,
ByzantineValidators: valSet.Validators[:nValidators/2],
}
assert.NotNil(t, lcae.String())
assert.NotNil(t, lcae.Hash())
assert.Equal(t, lcae.Height(), commonHeight) // Height should be the common Height
assert.NotNil(t, lcae.Bytes())
// maleate evidence to test hash uniqueness
testCases := []struct {
testName string
malleateEvidence func(*LightClientAttackEvidence)
}{
{"Different header", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock.Header = makeHeaderRandom() }},
{"Different common height", func(ev *LightClientAttackEvidence) {
ev.CommonHeight = height + 1
}},
}
for _, tc := range testCases {
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
},
ValidatorSet: valSet,
},
CommonHeight: commonHeight,
TotalVotingPower: valSet.TotalVotingPower(),
Timestamp: header.Time,
ByzantineValidators: valSet.Validators[:nValidators/2],
}
hash := lcae.Hash()
tc.malleateEvidence(lcae)
assert.NotEqual(t, hash, lcae.Hash(), tc.testName)
}
}
func TestLightClientAttackEvidenceValidation(t *testing.T) {
height := int64(5)
commonHeight := height - 1
nValidators := 10
voteSet, valSet, privVals := randVoteSet(height, 1, tmproto.PrecommitType, nValidators, 1)
header := makeHeaderRandom()
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())
require.NoError(t, err)
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
},
ValidatorSet: valSet,
},
CommonHeight: commonHeight,
TotalVotingPower: valSet.TotalVotingPower(),
Timestamp: header.Time,
ByzantineValidators: valSet.Validators[:nValidators/2],
}
assert.NoError(t, lcae.ValidateBasic())
testCases := []struct {
testName string
malleateEvidence func(*LightClientAttackEvidence)
expectErr bool
}{
{"Good LightClientAttackEvidence", func(ev *LightClientAttackEvidence) {}, false},
{"Negative height", func(ev *LightClientAttackEvidence) { ev.CommonHeight = -10 }, true},
{"Height is greater than divergent block", func(ev *LightClientAttackEvidence) {
ev.CommonHeight = height + 1
}, true},
{"Height is equal to the divergent block", func(ev *LightClientAttackEvidence) {
ev.CommonHeight = height
}, false},
{"Nil conflicting header", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock.Header = nil }, true},
{"Nil conflicting blocl", func(ev *LightClientAttackEvidence) { ev.ConflictingBlock = nil }, true},
{"Nil validator set", func(ev *LightClientAttackEvidence) {
ev.ConflictingBlock.ValidatorSet = &ValidatorSet{}
}, true},
{"Negative total voting power", func(ev *LightClientAttackEvidence) {
ev.TotalVotingPower = -1
}, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
},
ValidatorSet: valSet,
},
CommonHeight: commonHeight,
TotalVotingPower: valSet.TotalVotingPower(),
Timestamp: header.Time,
ByzantineValidators: valSet.Validators[:nValidators/2],
}
tc.malleateEvidence(lcae)
if tc.expectErr {
assert.Error(t, lcae.ValidateBasic(), tc.testName)
} else {
assert.NoError(t, lcae.ValidateBasic(), tc.testName)
}
})
}
}
func TestMockEvidenceValidateBasic(t *testing.T) {
goodEvidence := NewMockDuplicateVoteEvidence(int64(1), time.Now(), "mock-chain-id")
assert.Nil(t, goodEvidence.ValidateBasic())
}
func makeVote(
t *testing.T, val PrivValidator, chainID string, valIndex int32, height int64, round int32, step int, blockID BlockID,
time time.Time) *Vote {
pubKey, err := val.GetPubKey(context.Background())
require.NoError(t, err)
v := &Vote{
ValidatorAddress: pubKey.Address(),
ValidatorIndex: valIndex,
Height: height,
Round: round,
Type: tmproto.SignedMsgType(step),
BlockID: blockID,
Timestamp: time,
}
vpb := v.ToProto()
err = val.SignVote(context.Background(), chainID, vpb)
if err != nil {
panic(err)
}
v.Signature = vpb.Signature
return v
}
func makeHeaderRandom() *Header {
return &Header{
Version: version.Consensus{Block: version.BlockProtocol, App: 1},
ChainID: tmrand.Str(12),
Height: int64(mrand.Uint32() + 1),
Time: time.Now(),
LastBlockID: makeBlockIDRandom(),
LastCommitHash: crypto.CRandBytes(tmhash.Size),
DataHash: crypto.CRandBytes(tmhash.Size),
ValidatorsHash: crypto.CRandBytes(tmhash.Size),
NextValidatorsHash: crypto.CRandBytes(tmhash.Size),
ConsensusHash: crypto.CRandBytes(tmhash.Size),
AppHash: crypto.CRandBytes(tmhash.Size),
LastResultsHash: crypto.CRandBytes(tmhash.Size),
EvidenceHash: crypto.CRandBytes(tmhash.Size),
ProposerAddress: crypto.CRandBytes(crypto.AddressSize),
}
}
func TestEvidenceProto(t *testing.T) {
// -------- Votes --------
val := NewMockPV()
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
blockID2 := makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
const chainID = "mychain"
v := makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, 1, 0x01, blockID, defaultVoteTime)
v2 := makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, 2, 0x01, blockID2, defaultVoteTime)
tests := []struct {
testName string
evidence Evidence
toProtoErr bool
fromProtoErr bool
}{
{"nil fail", nil, true, true},
{"DuplicateVoteEvidence empty fail", &DuplicateVoteEvidence{}, false, true},
{"DuplicateVoteEvidence nil voteB", &DuplicateVoteEvidence{VoteA: v, VoteB: nil}, false, true},
{"DuplicateVoteEvidence nil voteA", &DuplicateVoteEvidence{VoteA: nil, VoteB: v}, false, true},
{"DuplicateVoteEvidence success", &DuplicateVoteEvidence{VoteA: v2, VoteB: v}, false, false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.testName, func(t *testing.T) {
pb, err := EvidenceToProto(tt.evidence)
if tt.toProtoErr {
assert.Error(t, err, tt.testName)
return
}
assert.NoError(t, err, tt.testName)
evi, err := EvidenceFromProto(pb)
if tt.fromProtoErr {
assert.Error(t, err, tt.testName)
return
}
require.Equal(t, tt.evidence, evi, tt.testName)
})
}
}
func TestEvidenceVectors(t *testing.T) {
// Votes for duplicateEvidence
val := NewMockPV()
val.PrivKey = ed25519.GenPrivKeyFromSecret([]byte("it's a secret")) // deterministic key
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
blockID2 := makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
const chainID = "mychain"
v := makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, 1, 0x01, blockID, defaultVoteTime)
v2 := makeVote(t, val, chainID, math.MaxInt32, math.MaxInt64, 2, 0x01, blockID2, defaultVoteTime)
// Data for LightClientAttackEvidence
height := int64(5)
commonHeight := height - 1
nValidators := 10
voteSet, valSet, privVals := deterministicVoteSet(height, 1, tmproto.PrecommitType, 1)
header := &Header{
Version: version.Consensus{Block: 1, App: 1},
ChainID: chainID,
Height: height,
Time: time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC),
LastBlockID: BlockID{},
LastCommitHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
DataHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
ValidatorsHash: valSet.Hash(),
NextValidatorsHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
ConsensusHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
AppHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
LastResultsHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
EvidenceHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
ProposerAddress: []byte("2915b7b15f979e48ebc61774bb1d86ba3136b7eb"),
}
blockID3 := makeBlockID(header.Hash(), math.MaxInt32, tmhash.Sum([]byte("partshash")))
commit, err := makeCommit(blockID3, height, 1, voteSet, privVals, defaultVoteTime)
require.NoError(t, err)
lcae := &LightClientAttackEvidence{
ConflictingBlock: &LightBlock{
SignedHeader: &SignedHeader{
Header: header,
Commit: commit,
},
ValidatorSet: valSet,
},
CommonHeight: commonHeight,
TotalVotingPower: valSet.TotalVotingPower(),
Timestamp: header.Time,
ByzantineValidators: valSet.Validators[:nValidators/2],
}
// assert.NoError(t, lcae.ValidateBasic())
testCases := []struct {
testName string
evList EvidenceList
expBytes string
}{
{"duplicateVoteEvidence",
EvidenceList{&DuplicateVoteEvidence{VoteA: v2, VoteB: v}},
"a9ce28d13bb31001fc3e5b7927051baf98f86abdbd64377643a304164c826923",
},
{"LightClientAttackEvidence",
EvidenceList{lcae},
"2f8782163c3905b26e65823ababc977fe54e97b94e60c0360b1e4726b668bb8e",
},
{"LightClientAttackEvidence & DuplicateVoteEvidence",
EvidenceList{&DuplicateVoteEvidence{VoteA: v2, VoteB: v}, lcae},
"eedb4b47d6dbc9d43f53da8aa50bb826e8d9fc7d897da777c8af6a04aa74163e",
},
}
for _, tc := range testCases {
tc := tc
hash := tc.evList.Hash()
require.Equal(t, tc.expBytes, hex.EncodeToString(hash), tc.testName)
}
}
+137
View File
@@ -0,0 +1,137 @@
package block
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"time"
"github.com/tendermint/tendermint/crypto"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmjson "github.com/tendermint/tendermint/libs/json"
tmtime "github.com/tendermint/tendermint/libs/time"
)
const (
// MaxChainIDLen is a maximum length of the chain ID.
MaxChainIDLen = 50
)
//------------------------------------------------------------
// core types for a genesis definition
// NOTE: any changes to the genesis definition should
// be reflected in the documentation:
// docs/tendermint-core/using-tendermint.md
// GenesisValidator is an initial validator.
type GenesisValidator struct {
Address Address `json:"address"`
PubKey crypto.PubKey `json:"pub_key"`
Power int64 `json:"power"`
Name string `json:"name"`
}
// GenesisDoc defines the initial conditions for a tendermint blockchain, in particular its validator set.
type GenesisDoc struct {
GenesisTime time.Time `json:"genesis_time"`
ChainID string `json:"chain_id"`
InitialHeight int64 `json:"initial_height"`
ConsensusParams *ConsensusParams `json:"consensus_params,omitempty"`
Validators []GenesisValidator `json:"validators,omitempty"`
AppHash tmbytes.HexBytes `json:"app_hash"`
AppState json.RawMessage `json:"app_state,omitempty"`
}
// SaveAs is a utility method for saving GenensisDoc as a JSON file.
func (genDoc *GenesisDoc) SaveAs(file string) error {
genDocBytes, err := tmjson.MarshalIndent(genDoc, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(file, genDocBytes, 0644) // nolint:gosec
}
// ValidatorHash returns the hash of the validator set contained in the GenesisDoc
func (genDoc *GenesisDoc) ValidatorHash() []byte {
vals := make([]*Validator, len(genDoc.Validators))
for i, v := range genDoc.Validators {
vals[i] = NewValidator(v.PubKey, v.Power)
}
vset := NewValidatorSet(vals)
return vset.Hash()
}
// ValidateAndComplete checks that all necessary fields are present
// and fills in defaults for optional fields left empty
func (genDoc *GenesisDoc) ValidateAndComplete() error {
if genDoc.ChainID == "" {
return errors.New("genesis doc must include non-empty chain_id")
}
if len(genDoc.ChainID) > MaxChainIDLen {
return fmt.Errorf("chain_id in genesis doc is too long (max: %d)", MaxChainIDLen)
}
if genDoc.InitialHeight < 0 {
return fmt.Errorf("initial_height cannot be negative (got %v)", genDoc.InitialHeight)
}
if genDoc.InitialHeight == 0 {
genDoc.InitialHeight = 1
}
if genDoc.ConsensusParams == nil {
genDoc.ConsensusParams = DefaultConsensusParams()
} else if err := genDoc.ConsensusParams.ValidateConsensusParams(); err != nil {
return err
}
for i, v := range genDoc.Validators {
if v.Power == 0 {
return fmt.Errorf("the genesis file cannot contain validators with no voting power: %v", v)
}
if len(v.Address) > 0 && !bytes.Equal(v.PubKey.Address(), v.Address) {
return fmt.Errorf("incorrect address for validator %v in the genesis file, should be %v", v, v.PubKey.Address())
}
if len(v.Address) == 0 {
genDoc.Validators[i].Address = v.PubKey.Address()
}
}
if genDoc.GenesisTime.IsZero() {
genDoc.GenesisTime = tmtime.Now()
}
return nil
}
//------------------------------------------------------------
// Make genesis state from file
// GenesisDocFromJSON unmarshalls JSON data into a GenesisDoc.
func GenesisDocFromJSON(jsonBlob []byte) (*GenesisDoc, error) {
genDoc := GenesisDoc{}
err := tmjson.Unmarshal(jsonBlob, &genDoc)
if err != nil {
return nil, err
}
if err := genDoc.ValidateAndComplete(); err != nil {
return nil, err
}
return &genDoc, err
}
// GenesisDocFromFile reads JSON data from a file and unmarshalls it into a GenesisDoc.
func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) {
jsonBlob, err := ioutil.ReadFile(genDocFile)
if err != nil {
return nil, fmt.Errorf("couldn't read GenesisDoc file: %w", err)
}
genDoc, err := GenesisDocFromJSON(jsonBlob)
if err != nil {
return nil, fmt.Errorf("error reading GenesisDoc at %s: %w", genDocFile, err)
}
return genDoc, nil
}
+165
View File
@@ -0,0 +1,165 @@
package block
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/ed25519"
tmjson "github.com/tendermint/tendermint/libs/json"
tmtime "github.com/tendermint/tendermint/libs/time"
)
func TestGenesisBad(t *testing.T) {
// test some bad ones from raw json
testCases := [][]byte{
{}, // empty
{1, 1, 1, 1, 1}, // junk
[]byte(`{}`), // empty
[]byte(`{"chain_id":"mychain","validators":[{}]}`), // invalid validator
[]byte(`{"chain_id":"chain","initial_height":"-1"}`), // negative initial height
// missing pub_key type
[]byte(
`{"validators":[{"pub_key":{"value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="},"power":"10","name":""}]}`,
),
// missing chain_id
[]byte(
`{"validators":[` +
`{"pub_key":{` +
`"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="` +
`},"power":"10","name":""}` +
`]}`,
),
// too big chain_id
[]byte(
`{"chain_id": "Lorem ipsum dolor sit amet, consectetuer adipiscing", "validators": [` +
`{"pub_key":{` +
`"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="` +
`},"power":"10","name":""}` +
`]}`,
),
// wrong address
[]byte(
`{"chain_id":"mychain", "validators":[` +
`{"address": "A", "pub_key":{` +
`"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="` +
`},"power":"10","name":""}` +
`]}`,
),
}
for _, testCase := range testCases {
_, err := GenesisDocFromJSON(testCase)
assert.Error(t, err, "expected error for empty genDoc json")
}
}
func TestGenesisGood(t *testing.T) {
// test a good one by raw json
genDocBytes := []byte(
`{
"genesis_time": "0001-01-01T00:00:00Z",
"chain_id": "test-chain-QDKdJr",
"initial_height": "1000",
"consensus_params": null,
"validators": [{
"pub_key":{"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="},
"power":"10",
"name":""
}],
"app_hash":"",
"app_state":{"account_owner": "Bob"}
}`,
)
_, err := GenesisDocFromJSON(genDocBytes)
assert.NoError(t, err, "expected no error for good genDoc json")
pubkey := ed25519.GenPrivKey().PubKey()
// create a base gendoc from struct
baseGenDoc := &GenesisDoc{
ChainID: "abc",
Validators: []GenesisValidator{{pubkey.Address(), pubkey, 10, "myval"}},
}
genDocBytes, err = tmjson.Marshal(baseGenDoc)
assert.NoError(t, err, "error marshaling genDoc")
// test base gendoc and check consensus params were filled
genDoc, err := GenesisDocFromJSON(genDocBytes)
assert.NoError(t, err, "expected no error for valid genDoc json")
assert.NotNil(t, genDoc.ConsensusParams, "expected consensus params to be filled in")
// check validator's address is filled
assert.NotNil(t, genDoc.Validators[0].Address, "expected validator's address to be filled in")
// create json with consensus params filled
genDocBytes, err = tmjson.Marshal(genDoc)
assert.NoError(t, err, "error marshaling genDoc")
genDoc, err = GenesisDocFromJSON(genDocBytes)
assert.NoError(t, err, "expected no error for valid genDoc json")
// test with invalid consensus params
genDoc.ConsensusParams.Block.MaxBytes = 0
genDocBytes, err = tmjson.Marshal(genDoc)
assert.NoError(t, err, "error marshaling genDoc")
_, err = GenesisDocFromJSON(genDocBytes)
assert.Error(t, err, "expected error for genDoc json with block size of 0")
// Genesis doc from raw json
missingValidatorsTestCases := [][]byte{
[]byte(`{"chain_id":"mychain"}`), // missing validators
[]byte(`{"chain_id":"mychain","validators":[]}`), // missing validators
[]byte(`{"chain_id":"mychain","validators":null}`), // nil validator
[]byte(`{"chain_id":"mychain"}`), // missing validators
}
for _, tc := range missingValidatorsTestCases {
_, err := GenesisDocFromJSON(tc)
assert.NoError(t, err)
}
}
func TestGenesisSaveAs(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "genesis")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
genDoc := randomGenesisDoc()
// save
err = genDoc.SaveAs(tmpfile.Name())
require.NoError(t, err)
stat, err := tmpfile.Stat()
require.NoError(t, err)
if err != nil && stat.Size() <= 0 {
t.Fatalf("SaveAs failed to write any bytes to %v", tmpfile.Name())
}
err = tmpfile.Close()
require.NoError(t, err)
// load
genDoc2, err := GenesisDocFromFile(tmpfile.Name())
require.NoError(t, err)
assert.EqualValues(t, genDoc2, genDoc)
assert.Equal(t, genDoc2.Validators, genDoc.Validators)
}
func TestGenesisValidatorHash(t *testing.T) {
genDoc := randomGenesisDoc()
assert.NotEmpty(t, genDoc.ValidatorHash())
}
func randomGenesisDoc() *GenesisDoc {
pubkey := ed25519.GenPrivKey().PubKey()
return &GenesisDoc{
GenesisTime: tmtime.Now(),
ChainID: "abc",
InitialHeight: 1000,
Validators: []GenesisValidator{{pubkey.Address(), pubkey, 10, "myval"}},
ConsensusParams: DefaultConsensusParams(),
AppHash: []byte{1, 2, 3},
}
}
+221
View File
@@ -0,0 +1,221 @@
package block
import (
"bytes"
"errors"
"fmt"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// LightBlock is a SignedHeader and a ValidatorSet.
// It is the basis of the light client
type LightBlock struct {
*SignedHeader `json:"signed_header"`
ValidatorSet *ValidatorSet `json:"validator_set"`
}
// ValidateBasic checks that the data is correct and consistent
//
// This does no verification of the signatures
func (lb LightBlock) ValidateBasic(chainID string) error {
if lb.SignedHeader == nil {
return errors.New("missing signed header")
}
if lb.ValidatorSet == nil {
return errors.New("missing validator set")
}
if err := lb.SignedHeader.ValidateBasic(chainID); err != nil {
return fmt.Errorf("invalid signed header: %w", err)
}
if err := lb.ValidatorSet.ValidateBasic(); err != nil {
return fmt.Errorf("invalid validator set: %w", err)
}
// make sure the validator set is consistent with the header
if valSetHash := lb.ValidatorSet.Hash(); !bytes.Equal(lb.SignedHeader.ValidatorsHash, valSetHash) {
return fmt.Errorf("expected validator hash of header to match validator set hash (%X != %X)",
lb.SignedHeader.ValidatorsHash, valSetHash,
)
}
return nil
}
// String returns a string representation of the LightBlock
func (lb LightBlock) String() string {
return lb.StringIndented("")
}
// StringIndented returns an indented string representation of the LightBlock
//
// SignedHeader
// ValidatorSet
func (lb LightBlock) StringIndented(indent string) string {
return fmt.Sprintf(`LightBlock{
%s %v
%s %v
%s}`,
indent, lb.SignedHeader.StringIndented(indent+" "),
indent, lb.ValidatorSet.StringIndented(indent+" "),
indent)
}
// ToProto converts the LightBlock to protobuf
func (lb *LightBlock) ToProto() (*tmproto.LightBlock, error) {
if lb == nil {
return nil, nil
}
lbp := new(tmproto.LightBlock)
var err error
if lb.SignedHeader != nil {
lbp.SignedHeader = lb.SignedHeader.ToProto()
}
if lb.ValidatorSet != nil {
lbp.ValidatorSet, err = lb.ValidatorSet.ToProto()
if err != nil {
return nil, err
}
}
return lbp, nil
}
// LightBlockFromProto converts from protobuf back into the Lightblock.
// An error is returned if either the validator set or signed header are invalid
func LightBlockFromProto(pb *tmproto.LightBlock) (*LightBlock, error) {
if pb == nil {
return nil, errors.New("nil light block")
}
lb := new(LightBlock)
if pb.SignedHeader != nil {
sh, err := SignedHeaderFromProto(pb.SignedHeader)
if err != nil {
return nil, err
}
lb.SignedHeader = sh
}
if pb.ValidatorSet != nil {
vals, err := ValidatorSetFromProto(pb.ValidatorSet)
if err != nil {
return nil, err
}
lb.ValidatorSet = vals
}
return lb, nil
}
//-----------------------------------------------------------------------------
// SignedHeader is a header along with the commits that prove it.
type SignedHeader struct {
*Header `json:"header"`
Commit *Commit `json:"commit"`
}
// ValidateBasic does basic consistency checks and makes sure the header
// and commit are consistent.
//
// NOTE: This does not actually check the cryptographic signatures. Make sure
// to use a Verifier to validate the signatures actually provide a
// significantly strong proof for this header's validity.
func (sh SignedHeader) ValidateBasic(chainID string) error {
if sh.Header == nil {
return errors.New("missing header")
}
if sh.Commit == nil {
return errors.New("missing commit")
}
if err := sh.Header.ValidateBasic(); err != nil {
return fmt.Errorf("invalid header: %w", err)
}
if err := sh.Commit.ValidateBasic(); err != nil {
return fmt.Errorf("invalid commit: %w", err)
}
if sh.ChainID != chainID {
return fmt.Errorf("header belongs to another chain %q, not %q", sh.ChainID, chainID)
}
// Make sure the header is consistent with the commit.
if sh.Commit.Height != sh.Height {
return fmt.Errorf("header and commit height mismatch: %d vs %d", sh.Height, sh.Commit.Height)
}
if hhash, chash := sh.Header.Hash(), sh.Commit.BlockID.Hash; !bytes.Equal(hhash, chash) {
return fmt.Errorf("commit signs block %X, header is block %X", chash, hhash)
}
return nil
}
// String returns a string representation of SignedHeader.
func (sh SignedHeader) String() string {
return sh.StringIndented("")
}
// StringIndented returns an indented string representation of SignedHeader.
//
// Header
// Commit
func (sh SignedHeader) StringIndented(indent string) string {
return fmt.Sprintf(`SignedHeader{
%s %v
%s %v
%s}`,
indent, sh.Header.StringIndented(indent+" "),
indent, sh.Commit.StringIndented(indent+" "),
indent)
}
// ToProto converts SignedHeader to protobuf
func (sh *SignedHeader) ToProto() *tmproto.SignedHeader {
if sh == nil {
return nil
}
psh := new(tmproto.SignedHeader)
if sh.Header != nil {
psh.Header = sh.Header.ToProto()
}
if sh.Commit != nil {
psh.Commit = sh.Commit.ToProto()
}
return psh
}
// FromProto sets a protobuf SignedHeader to the given pointer.
// It returns an error if the header or the commit is invalid.
func SignedHeaderFromProto(shp *tmproto.SignedHeader) (*SignedHeader, error) {
if shp == nil {
return nil, errors.New("nil SignedHeader")
}
sh := new(SignedHeader)
if shp.Header != nil {
h, err := HeaderFromProto(shp.Header)
if err != nil {
return nil, err
}
sh.Header = &h
}
if shp.Commit != nil {
c, err := CommitFromProto(shp.Commit)
if err != nil {
return nil, err
}
sh.Commit = c
}
return sh, nil
}
+165
View File
@@ -0,0 +1,165 @@
package block
import (
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/version"
)
func TestLightBlockValidateBasic(t *testing.T) {
header := MakeRandHeader()
commit := randCommit(time.Now())
vals, _ := randValidatorPrivValSet(5, 1)
header.Height = commit.Height
header.LastBlockID = commit.BlockID
header.ValidatorsHash = vals.Hash()
header.Version.Block = version.BlockProtocol
vals2, _ := randValidatorPrivValSet(3, 1)
vals3 := vals.Copy()
vals3.Proposer = &Validator{}
commit.BlockID.Hash = header.Hash()
sh := &SignedHeader{
Header: &header,
Commit: commit,
}
testCases := []struct {
name string
sh *SignedHeader
vals *ValidatorSet
expectErr bool
}{
{"valid light block", sh, vals, false},
{"hashes don't match", sh, vals2, true},
{"invalid validator set", sh, vals3, true},
{"invalid signed header", &SignedHeader{Header: &header, Commit: randCommit(time.Now())}, vals, true},
}
for _, tc := range testCases {
lightBlock := LightBlock{
SignedHeader: tc.sh,
ValidatorSet: tc.vals,
}
err := lightBlock.ValidateBasic(header.ChainID)
if tc.expectErr {
assert.Error(t, err, tc.name)
} else {
assert.NoError(t, err, tc.name)
}
}
}
func TestLightBlockProtobuf(t *testing.T) {
header := MakeRandHeader()
commit := randCommit(time.Now())
vals, _ := randValidatorPrivValSet(5, 1)
header.Height = commit.Height
header.LastBlockID = commit.BlockID
header.Version.Block = version.BlockProtocol
header.ValidatorsHash = vals.Hash()
vals3 := vals.Copy()
vals3.Proposer = &Validator{}
commit.BlockID.Hash = header.Hash()
sh := &SignedHeader{
Header: &header,
Commit: commit,
}
testCases := []struct {
name string
sh *SignedHeader
vals *ValidatorSet
toProtoErr bool
toBlockErr bool
}{
{"valid light block", sh, vals, false, false},
{"empty signed header", &SignedHeader{}, vals, false, false},
{"empty validator set", sh, &ValidatorSet{}, false, true},
{"empty light block", &SignedHeader{}, &ValidatorSet{}, false, true},
}
for _, tc := range testCases {
lightBlock := &LightBlock{
SignedHeader: tc.sh,
ValidatorSet: tc.vals,
}
lbp, err := lightBlock.ToProto()
if tc.toProtoErr {
assert.Error(t, err, tc.name)
} else {
assert.NoError(t, err, tc.name)
}
lb, err := LightBlockFromProto(lbp)
if tc.toBlockErr {
assert.Error(t, err, tc.name)
} else {
assert.NoError(t, err, tc.name)
assert.Equal(t, lightBlock, lb)
}
}
}
func TestSignedHeaderValidateBasic(t *testing.T) {
commit := randCommit(time.Now())
chainID := "𠜎"
timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC)
h := Header{
Version: version.Consensus{Block: version.BlockProtocol, App: math.MaxInt64},
ChainID: chainID,
Height: commit.Height,
Time: timestamp,
LastBlockID: commit.BlockID,
LastCommitHash: commit.Hash(),
DataHash: commit.Hash(),
ValidatorsHash: commit.Hash(),
NextValidatorsHash: commit.Hash(),
ConsensusHash: commit.Hash(),
AppHash: commit.Hash(),
LastResultsHash: commit.Hash(),
EvidenceHash: commit.Hash(),
ProposerAddress: crypto.AddressHash([]byte("proposer_address")),
}
validSignedHeader := SignedHeader{Header: &h, Commit: commit}
validSignedHeader.Commit.BlockID.Hash = validSignedHeader.Hash()
invalidSignedHeader := SignedHeader{}
testCases := []struct {
testName string
shHeader *Header
shCommit *Commit
expectErr bool
}{
{"Valid Signed Header", validSignedHeader.Header, validSignedHeader.Commit, false},
{"Invalid Signed Header", invalidSignedHeader.Header, validSignedHeader.Commit, true},
{"Invalid Signed Header", validSignedHeader.Header, invalidSignedHeader.Commit, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
sh := SignedHeader{
Header: tc.shHeader,
Commit: tc.shCommit,
}
err := sh.ValidateBasic(validSignedHeader.Header.ChainID)
assert.Equalf(
t,
tc.expectErr,
err != nil,
"Validate Basic had an unexpected result",
err,
)
})
}
}
+279
View File
@@ -0,0 +1,279 @@
package block
import (
"errors"
"fmt"
"time"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/crypto/sr25519"
"github.com/tendermint/tendermint/crypto/tmhash"
tmstrings "github.com/tendermint/tendermint/libs/strings"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
const (
// MaxBlockSizeBytes is the maximum permitted size of the blocks.
MaxBlockSizeBytes = 104857600 // 100MB
// BlockPartSizeBytes is the size of one block part.
BlockPartSizeBytes uint32 = 65536 // 64kB
// MaxBlockPartsCount is the maximum number of block parts.
MaxBlockPartsCount = (MaxBlockSizeBytes / BlockPartSizeBytes) + 1
ABCIPubKeyTypeEd25519 = ed25519.KeyType
ABCIPubKeyTypeSecp256k1 = secp256k1.KeyType
ABCIPubKeyTypeSr25519 = sr25519.KeyType
)
var ABCIPubKeyTypesToNames = map[string]string{
ABCIPubKeyTypeEd25519: ed25519.PubKeyName,
ABCIPubKeyTypeSecp256k1: secp256k1.PubKeyName,
ABCIPubKeyTypeSr25519: sr25519.PubKeyName,
}
// ConsensusParams contains consensus critical parameters that determine the
// validity of blocks.
type ConsensusParams struct {
Block BlockParams `json:"block"`
Evidence EvidenceParams `json:"evidence"`
Validator ValidatorParams `json:"validator"`
Version VersionParams `json:"version"`
}
// HashedParams is a subset of ConsensusParams.
// It is amino encoded and hashed into
// the Header.ConsensusHash.
type HashedParams struct {
BlockMaxBytes int64
BlockMaxGas int64
}
// BlockParams define limits on the block size and gas plus minimum time
// between blocks.
type BlockParams struct {
MaxBytes int64 `json:"max_bytes"`
MaxGas int64 `json:"max_gas"`
}
// EvidenceParams determine how we handle evidence of malfeasance.
type EvidenceParams struct {
MaxAgeNumBlocks int64 `json:"max_age_num_blocks"` // only accept new evidence more recent than this
MaxAgeDuration time.Duration `json:"max_age_duration"`
MaxBytes int64 `json:"max_bytes"`
}
// ValidatorParams restrict the public key types validators can use.
// NOTE: uses ABCI pubkey naming, not Amino names.
type ValidatorParams struct {
PubKeyTypes []string `json:"pub_key_types"`
}
type VersionParams struct {
AppVersion uint64 `json:"app_version"`
}
// DefaultConsensusParams returns a default ConsensusParams.
func DefaultConsensusParams() *ConsensusParams {
return &ConsensusParams{
Block: DefaultBlockParams(),
Evidence: DefaultEvidenceParams(),
Validator: DefaultValidatorParams(),
Version: DefaultVersionParams(),
}
}
// DefaultBlockParams returns a default BlockParams.
func DefaultBlockParams() BlockParams {
return BlockParams{
MaxBytes: 22020096, // 21MB
MaxGas: -1,
}
}
// DefaultEvidenceParams returns a default EvidenceParams.
func DefaultEvidenceParams() EvidenceParams {
return EvidenceParams{
MaxAgeNumBlocks: 100000, // 27.8 hrs at 1block/s
MaxAgeDuration: 48 * time.Hour,
MaxBytes: 1048576, // 1MB
}
}
// DefaultValidatorParams returns a default ValidatorParams, which allows
// only ed25519 pubkeys.
func DefaultValidatorParams() ValidatorParams {
return ValidatorParams{
PubKeyTypes: []string{ABCIPubKeyTypeEd25519},
}
}
func DefaultVersionParams() VersionParams {
return VersionParams{
AppVersion: 0,
}
}
func (val *ValidatorParams) IsValidPubkeyType(pubkeyType string) bool {
for i := 0; i < len(val.PubKeyTypes); i++ {
if val.PubKeyTypes[i] == pubkeyType {
return true
}
}
return false
}
// Validate validates the ConsensusParams to ensure all values are within their
// allowed limits, and returns an error if they are not.
func (params ConsensusParams) ValidateConsensusParams() error {
if params.Block.MaxBytes <= 0 {
return fmt.Errorf("block.MaxBytes must be greater than 0. Got %d",
params.Block.MaxBytes)
}
if params.Block.MaxBytes > MaxBlockSizeBytes {
return fmt.Errorf("block.MaxBytes is too big. %d > %d",
params.Block.MaxBytes, MaxBlockSizeBytes)
}
if params.Block.MaxGas < -1 {
return fmt.Errorf("block.MaxGas must be greater or equal to -1. Got %d",
params.Block.MaxGas)
}
if params.Evidence.MaxAgeNumBlocks <= 0 {
return fmt.Errorf("evidence.MaxAgeNumBlocks must be greater than 0. Got %d",
params.Evidence.MaxAgeNumBlocks)
}
if params.Evidence.MaxAgeDuration <= 0 {
return fmt.Errorf("evidence.MaxAgeDuration must be grater than 0 if provided, Got %v",
params.Evidence.MaxAgeDuration)
}
if params.Evidence.MaxBytes > params.Block.MaxBytes {
return fmt.Errorf("evidence.MaxBytesEvidence is greater than upper bound, %d > %d",
params.Evidence.MaxBytes, params.Block.MaxBytes)
}
if params.Evidence.MaxBytes < 0 {
return fmt.Errorf("evidence.MaxBytes must be non negative. Got: %d",
params.Evidence.MaxBytes)
}
if len(params.Validator.PubKeyTypes) == 0 {
return errors.New("len(Validator.PubKeyTypes) must be greater than 0")
}
// Check if keyType is a known ABCIPubKeyType
for i := 0; i < len(params.Validator.PubKeyTypes); i++ {
keyType := params.Validator.PubKeyTypes[i]
if _, ok := ABCIPubKeyTypesToNames[keyType]; !ok {
return fmt.Errorf("params.Validator.PubKeyTypes[%d], %s, is an unknown pubkey type",
i, keyType)
}
}
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
// protocol. No need for a Merkle tree here, just a small struct to hash.
func (params ConsensusParams) HashConsensusParams() []byte {
hasher := tmhash.New()
hp := tmproto.HashedParams{
BlockMaxBytes: params.Block.MaxBytes,
BlockMaxGas: params.Block.MaxGas,
}
bz, err := hp.Marshal()
if err != nil {
panic(err)
}
_, err = hasher.Write(bz)
if err != nil {
panic(err)
}
return hasher.Sum(nil)
}
func (params *ConsensusParams) Equals(params2 *ConsensusParams) bool {
return params.Block == params2.Block &&
params.Evidence == params2.Evidence &&
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) UpdateConsensusParams(params2 *tmproto.ConsensusParams) ConsensusParams {
res := params // explicit copy
if params2 == nil {
return res
}
// we must defensively consider any structs may be nil
if params2.Block != nil {
res.Block.MaxBytes = params2.Block.MaxBytes
res.Block.MaxGas = params2.Block.MaxGas
}
if params2.Evidence != nil {
res.Evidence.MaxAgeNumBlocks = params2.Evidence.MaxAgeNumBlocks
res.Evidence.MaxAgeDuration = params2.Evidence.MaxAgeDuration
res.Evidence.MaxBytes = params2.Evidence.MaxBytes
}
if params2.Validator != nil {
// Copy params2.Validator.PubkeyTypes, and set result's value to the copy.
// This avoids having to initialize the slice to 0 values, and then write to it again.
res.Validator.PubKeyTypes = append([]string{}, params2.Validator.PubKeyTypes...)
}
if params2.Version != nil {
res.Version.AppVersion = params2.Version.AppVersion
}
return res
}
func (params *ConsensusParams) ToProto() tmproto.ConsensusParams {
return tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxBytes: params.Block.MaxBytes,
MaxGas: params.Block.MaxGas,
},
Evidence: &tmproto.EvidenceParams{
MaxAgeNumBlocks: params.Evidence.MaxAgeNumBlocks,
MaxAgeDuration: params.Evidence.MaxAgeDuration,
MaxBytes: params.Evidence.MaxBytes,
},
Validator: &tmproto.ValidatorParams{
PubKeyTypes: params.Validator.PubKeyTypes,
},
Version: &tmproto.VersionParams{
AppVersion: params.Version.AppVersion,
},
}
}
func ConsensusParamsFromProto(pbParams tmproto.ConsensusParams) ConsensusParams {
return ConsensusParams{
Block: BlockParams{
MaxBytes: pbParams.Block.MaxBytes,
MaxGas: pbParams.Block.MaxGas,
},
Evidence: EvidenceParams{
MaxAgeNumBlocks: pbParams.Evidence.MaxAgeNumBlocks,
MaxAgeDuration: pbParams.Evidence.MaxAgeDuration,
MaxBytes: pbParams.Evidence.MaxBytes,
},
Validator: ValidatorParams{
PubKeyTypes: pbParams.Validator.PubKeyTypes,
},
Version: VersionParams{
AppVersion: pbParams.Version.AppVersion,
},
}
}
+188
View File
@@ -0,0 +1,188 @@
package block
import (
"bytes"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
var (
valEd25519 = []string{ABCIPubKeyTypeEd25519}
valSecp256k1 = []string{ABCIPubKeyTypeSecp256k1}
valSr25519 = []string{ABCIPubKeyTypeSr25519}
)
func TestConsensusParamsValidation(t *testing.T) {
testCases := []struct {
params ConsensusParams
valid bool
}{
// test block params
0: {makeParams(1, 0, 2, 0, valEd25519), true},
1: {makeParams(0, 0, 2, 0, valEd25519), false},
2: {makeParams(47*1024*1024, 0, 2, 0, valEd25519), true},
3: {makeParams(10, 0, 2, 0, valEd25519), true},
4: {makeParams(100*1024*1024, 0, 2, 0, valEd25519), true},
5: {makeParams(101*1024*1024, 0, 2, 0, valEd25519), false},
6: {makeParams(1024*1024*1024, 0, 2, 0, valEd25519), false},
7: {makeParams(1024*1024*1024, 0, -1, 0, valEd25519), false},
// test evidence params
8: {makeParams(1, 0, 0, 0, valEd25519), false},
9: {makeParams(1, 0, 2, 2, valEd25519), false},
10: {makeParams(1000, 0, 2, 1, valEd25519), true},
11: {makeParams(1, 0, -1, 0, valEd25519), false},
// test no pubkey type provided
12: {makeParams(1, 0, 2, 0, []string{}), false},
// test invalid pubkey type provided
13: {makeParams(1, 0, 2, 0, []string{"potatoes make good pubkeys"}), false},
}
for i, tc := range testCases {
if tc.valid {
assert.NoErrorf(t, tc.params.ValidateConsensusParams(), "expected no error for valid params (#%d)", i)
} else {
assert.Errorf(t, tc.params.ValidateConsensusParams(), "expected error for non valid params (#%d)", i)
}
}
}
func makeParams(
blockBytes, blockGas int64,
evidenceAge int64,
maxEvidenceBytes int64,
pubkeyTypes []string,
) ConsensusParams {
return ConsensusParams{
Block: BlockParams{
MaxBytes: blockBytes,
MaxGas: blockGas,
},
Evidence: EvidenceParams{
MaxAgeNumBlocks: evidenceAge,
MaxAgeDuration: time.Duration(evidenceAge),
MaxBytes: maxEvidenceBytes,
},
Validator: ValidatorParams{
PubKeyTypes: pubkeyTypes,
},
}
}
func TestConsensusParamsHash(t *testing.T) {
params := []ConsensusParams{
makeParams(4, 2, 3, 1, valEd25519),
makeParams(1, 4, 3, 1, valEd25519),
makeParams(1, 2, 4, 1, valEd25519),
makeParams(2, 5, 7, 1, valEd25519),
makeParams(1, 7, 6, 1, valEd25519),
makeParams(9, 5, 4, 1, valEd25519),
makeParams(7, 8, 9, 1, valEd25519),
makeParams(4, 6, 5, 1, valEd25519),
}
hashes := make([][]byte, len(params))
for i := range params {
hashes[i] = params[i].HashConsensusParams()
}
// make sure there are no duplicates...
// sort, then check in order for matches
sort.Slice(hashes, func(i, j int) bool {
return bytes.Compare(hashes[i], hashes[j]) < 0
})
for i := 0; i < len(hashes)-1; i++ {
assert.NotEqual(t, hashes[i], hashes[i+1])
}
}
func TestConsensusParamsUpdate(t *testing.T) {
testCases := []struct {
params ConsensusParams
updates *tmproto.ConsensusParams
updatedParams ConsensusParams
}{
// empty updates
{
makeParams(1, 2, 3, 0, valEd25519),
&tmproto.ConsensusParams{},
makeParams(1, 2, 3, 0, valEd25519),
},
// fine updates
{
makeParams(1, 2, 3, 0, valEd25519),
&tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxBytes: 100,
MaxGas: 200,
},
Evidence: &tmproto.EvidenceParams{
MaxAgeNumBlocks: 300,
MaxAgeDuration: time.Duration(300),
MaxBytes: 50,
},
Validator: &tmproto.ValidatorParams{
PubKeyTypes: valSecp256k1,
},
},
makeParams(100, 200, 300, 50, valSecp256k1),
},
{
makeParams(1, 2, 3, 0, valEd25519),
&tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxBytes: 100,
MaxGas: 200,
},
Evidence: &tmproto.EvidenceParams{
MaxAgeNumBlocks: 300,
MaxAgeDuration: time.Duration(300),
MaxBytes: 50,
},
Validator: &tmproto.ValidatorParams{
PubKeyTypes: valSr25519,
},
}, makeParams(100, 200, 300, 50, valSr25519),
},
}
for _, tc := range testCases {
assert.Equal(t, tc.updatedParams, tc.params.UpdateConsensusParams(tc.updates))
}
}
func TestConsensusParamsUpdate_AppVersion(t *testing.T) {
params := makeParams(1, 2, 3, 0, valEd25519)
assert.EqualValues(t, 0, params.Version.AppVersion)
updated := params.UpdateConsensusParams(
&tmproto.ConsensusParams{Version: &tmproto.VersionParams{AppVersion: 1}})
assert.EqualValues(t, 1, updated.Version.AppVersion)
}
func TestProto(t *testing.T) {
params := []ConsensusParams{
makeParams(4, 2, 3, 1, valEd25519),
makeParams(1, 4, 3, 1, valEd25519),
makeParams(1, 2, 4, 1, valEd25519),
makeParams(2, 5, 7, 1, valEd25519),
makeParams(1, 7, 6, 1, valEd25519),
makeParams(9, 5, 4, 1, valEd25519),
makeParams(7, 8, 9, 1, valEd25519),
makeParams(4, 6, 5, 1, valEd25519),
}
for i := range params {
pbParams := params[i].ToProto()
oriParams := ConsensusParamsFromProto(pbParams)
assert.Equal(t, params[i], oriParams)
}
}
+375
View File
@@ -0,0 +1,375 @@
package block
import (
"bytes"
"errors"
"fmt"
"io"
"github.com/tendermint/tendermint/crypto/merkle"
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
"github.com/tendermint/tendermint/libs/bits"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmjson "github.com/tendermint/tendermint/libs/json"
tmmath "github.com/tendermint/tendermint/libs/math"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
var (
ErrPartSetUnexpectedIndex = errors.New("error part set unexpected index")
ErrPartSetInvalidProof = errors.New("error part set invalid proof")
)
type Part struct {
Index uint32 `json:"index"`
Bytes tmbytes.HexBytes `json:"bytes"`
Proof merkle.Proof `json:"proof"`
}
// ValidateBasic performs basic validation.
func (part *Part) ValidateBasic() error {
if len(part.Bytes) > int(BlockPartSizeBytes) {
return fmt.Errorf("too big: %d bytes, max: %d", len(part.Bytes), BlockPartSizeBytes)
}
if err := part.Proof.ValidateBasic(); err != nil {
return fmt.Errorf("wrong Proof: %w", err)
}
return nil
}
// String returns a string representation of Part.
//
// See StringIndented.
func (part *Part) String() string {
return part.StringIndented("")
}
// StringIndented returns an indented Part.
//
// See merkle.Proof#StringIndented
func (part *Part) StringIndented(indent string) string {
return fmt.Sprintf(`Part{#%v
%s Bytes: %X...
%s Proof: %v
%s}`,
part.Index,
indent, tmbytes.Fingerprint(part.Bytes),
indent, part.Proof.StringIndented(indent+" "),
indent)
}
func (part *Part) ToProto() (*tmproto.Part, error) {
if part == nil {
return nil, errors.New("nil part")
}
pb := new(tmproto.Part)
proof := part.Proof.ToProto()
pb.Index = part.Index
pb.Bytes = part.Bytes
pb.Proof = *proof
return pb, nil
}
func PartFromProto(pb *tmproto.Part) (*Part, error) {
if pb == nil {
return nil, errors.New("nil part")
}
part := new(Part)
proof, err := merkle.ProofFromProto(&pb.Proof)
if err != nil {
return nil, err
}
part.Index = pb.Index
part.Bytes = pb.Bytes
part.Proof = *proof
return part, part.ValidateBasic()
}
//-------------------------------------
type PartSetHeader struct {
Total uint32 `json:"total"`
Hash tmbytes.HexBytes `json:"hash"`
}
// String returns a string representation of PartSetHeader.
//
// 1. total number of parts
// 2. first 6 bytes of the hash
func (psh PartSetHeader) String() string {
return fmt.Sprintf("%v:%X", psh.Total, tmbytes.Fingerprint(psh.Hash))
}
func (psh PartSetHeader) IsZero() bool {
return psh.Total == 0 && len(psh.Hash) == 0
}
func (psh PartSetHeader) Equals(other PartSetHeader) bool {
return psh.Total == other.Total && bytes.Equal(psh.Hash, other.Hash)
}
// ValidateBasic performs basic validation.
func (psh PartSetHeader) ValidateBasic() error {
// Hash can be empty in case of POLBlockID.PartSetHeader in Proposal.
if err := ValidateHash(psh.Hash); err != nil {
return fmt.Errorf("wrong Hash: %w", err)
}
return nil
}
// ToProto converts PartSetHeader to protobuf
func (psh *PartSetHeader) ToProto() tmproto.PartSetHeader {
if psh == nil {
return tmproto.PartSetHeader{}
}
return tmproto.PartSetHeader{
Total: psh.Total,
Hash: psh.Hash,
}
}
// FromProto sets a protobuf PartSetHeader to the given pointer
func PartSetHeaderFromProto(ppsh *tmproto.PartSetHeader) (*PartSetHeader, error) {
if ppsh == nil {
return nil, errors.New("nil PartSetHeader")
}
psh := new(PartSetHeader)
psh.Total = ppsh.Total
psh.Hash = ppsh.Hash
return psh, psh.ValidateBasic()
}
//-------------------------------------
type PartSet struct {
total uint32
hash []byte
mtx tmsync.Mutex
parts []*Part
partsBitArray *bits.BitArray
count uint32
// a count of the total size (in bytes). Used to ensure that the
// part set doesn't exceed the maximum block bytes
byteSize int64
}
// Returns an immutable, full PartSet from the data bytes.
// The data bytes are split into "partSize" chunks, and merkle tree computed.
// CONTRACT: partSize is greater than zero.
func NewPartSetFromData(data []byte, partSize uint32) *PartSet {
// divide data into 4kb parts.
total := (uint32(len(data)) + partSize - 1) / partSize
parts := make([]*Part, total)
partsBytes := make([][]byte, total)
partsBitArray := bits.NewBitArray(int(total))
for i := uint32(0); i < total; i++ {
part := &Part{
Index: i,
Bytes: data[i*partSize : tmmath.MinInt(len(data), int((i+1)*partSize))],
}
parts[i] = part
partsBytes[i] = part.Bytes
partsBitArray.SetIndex(int(i), true)
}
// Compute merkle proofs
root, proofs := merkle.ProofsFromByteSlices(partsBytes)
for i := uint32(0); i < total; i++ {
parts[i].Proof = *proofs[i]
}
return &PartSet{
total: total,
hash: root,
parts: parts,
partsBitArray: partsBitArray,
count: total,
byteSize: int64(len(data)),
}
}
// Returns an empty PartSet ready to be populated.
func NewPartSetFromHeader(header PartSetHeader) *PartSet {
return &PartSet{
total: header.Total,
hash: header.Hash,
parts: make([]*Part, header.Total),
partsBitArray: bits.NewBitArray(int(header.Total)),
count: 0,
byteSize: 0,
}
}
func (ps *PartSet) Header() PartSetHeader {
if ps == nil {
return PartSetHeader{}
}
return PartSetHeader{
Total: ps.total,
Hash: ps.hash,
}
}
func (ps *PartSet) HasHeader(header PartSetHeader) bool {
if ps == nil {
return false
}
return ps.Header().Equals(header)
}
func (ps *PartSet) BitArray() *bits.BitArray {
ps.mtx.Lock()
defer ps.mtx.Unlock()
return ps.partsBitArray.Copy()
}
func (ps *PartSet) Hash() []byte {
if ps == nil {
return merkle.HashFromByteSlices(nil)
}
return ps.hash
}
func (ps *PartSet) HashesTo(hash []byte) bool {
if ps == nil {
return false
}
return bytes.Equal(ps.hash, hash)
}
func (ps *PartSet) Count() uint32 {
if ps == nil {
return 0
}
return ps.count
}
func (ps *PartSet) ByteSize() int64 {
if ps == nil {
return 0
}
return ps.byteSize
}
func (ps *PartSet) Total() uint32 {
if ps == nil {
return 0
}
return ps.total
}
func (ps *PartSet) AddPart(part *Part) (bool, error) {
if ps == nil {
return false, nil
}
ps.mtx.Lock()
defer ps.mtx.Unlock()
// Invalid part index
if part.Index >= ps.total {
return false, ErrPartSetUnexpectedIndex
}
// If part already exists, return false.
if ps.parts[part.Index] != nil {
return false, nil
}
// Check hash proof
if part.Proof.Verify(ps.Hash(), part.Bytes) != nil {
return false, ErrPartSetInvalidProof
}
// Add part
ps.parts[part.Index] = part
ps.partsBitArray.SetIndex(int(part.Index), true)
ps.count++
ps.byteSize += int64(len(part.Bytes))
return true, nil
}
func (ps *PartSet) GetPart(index int) *Part {
ps.mtx.Lock()
defer ps.mtx.Unlock()
return ps.parts[index]
}
func (ps *PartSet) IsComplete() bool {
return ps.count == ps.total
}
func (ps *PartSet) GetReader() io.Reader {
if !ps.IsComplete() {
panic("Cannot GetReader() on incomplete PartSet")
}
return NewPartSetReader(ps.parts)
}
type PartSetReader struct {
i int
parts []*Part
reader *bytes.Reader
}
func NewPartSetReader(parts []*Part) *PartSetReader {
return &PartSetReader{
i: 0,
parts: parts,
reader: bytes.NewReader(parts[0].Bytes),
}
}
func (psr *PartSetReader) Read(p []byte) (n int, err error) {
readerLen := psr.reader.Len()
if readerLen >= len(p) {
return psr.reader.Read(p)
} else if readerLen > 0 {
n1, err := psr.Read(p[:readerLen])
if err != nil {
return n1, err
}
n2, err := psr.Read(p[readerLen:])
return n1 + n2, err
}
psr.i++
if psr.i >= len(psr.parts) {
return 0, io.EOF
}
psr.reader = bytes.NewReader(psr.parts[psr.i].Bytes)
return psr.Read(p)
}
// StringShort returns a short version of String.
//
// (Count of Total)
func (ps *PartSet) StringShort() string {
if ps == nil {
return "nil-PartSet"
}
ps.mtx.Lock()
defer ps.mtx.Unlock()
return fmt.Sprintf("(%v of %v)", ps.Count(), ps.Total())
}
func (ps *PartSet) MarshalJSON() ([]byte, error) {
if ps == nil {
return []byte("{}"), nil
}
ps.mtx.Lock()
defer ps.mtx.Unlock()
return tmjson.Marshal(struct {
CountTotal string `json:"count/total"`
PartsBitArray *bits.BitArray `json:"parts_bit_array"`
}{
fmt.Sprintf("%d/%d", ps.Count(), ps.Total()),
ps.partsBitArray,
})
}
+194
View File
@@ -0,0 +1,194 @@
package block
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/merkle"
tmrand "github.com/tendermint/tendermint/libs/rand"
)
const (
testPartSize = 65536 // 64KB ... 4096 // 4KB
)
func TestBasicPartSet(t *testing.T) {
// Construct random data of size partSize * 100
nParts := 100
data := tmrand.Bytes(testPartSize * nParts)
partSet := NewPartSetFromData(data, testPartSize)
assert.NotEmpty(t, partSet.Hash())
assert.EqualValues(t, nParts, partSet.Total())
assert.Equal(t, nParts, partSet.BitArray().Size())
assert.True(t, partSet.HashesTo(partSet.Hash()))
assert.True(t, partSet.IsComplete())
assert.EqualValues(t, nParts, partSet.Count())
assert.EqualValues(t, testPartSize*nParts, partSet.ByteSize())
// Test adding parts to a new partSet.
partSet2 := NewPartSetFromHeader(partSet.Header())
assert.True(t, partSet2.HasHeader(partSet.Header()))
for i := 0; i < int(partSet.Total()); i++ {
part := partSet.GetPart(i)
// t.Logf("\n%v", part)
added, err := partSet2.AddPart(part)
if !added || err != nil {
t.Errorf("failed to add part %v, error: %v", i, err)
}
}
// adding part with invalid index
added, err := partSet2.AddPart(&Part{Index: 10000})
assert.False(t, added)
assert.Error(t, err)
// adding existing part
added, err = partSet2.AddPart(partSet2.GetPart(0))
assert.False(t, added)
assert.Nil(t, err)
assert.Equal(t, partSet.Hash(), partSet2.Hash())
assert.EqualValues(t, nParts, partSet2.Total())
assert.EqualValues(t, nParts*testPartSize, partSet.ByteSize())
assert.True(t, partSet2.IsComplete())
// Reconstruct data, assert that they are equal.
data2Reader := partSet2.GetReader()
data2, err := ioutil.ReadAll(data2Reader)
require.NoError(t, err)
assert.Equal(t, data, data2)
}
func TestWrongProof(t *testing.T) {
// Construct random data of size partSize * 100
data := tmrand.Bytes(testPartSize * 100)
partSet := NewPartSetFromData(data, testPartSize)
// Test adding a part with wrong data.
partSet2 := NewPartSetFromHeader(partSet.Header())
// Test adding a part with wrong trail.
part := partSet.GetPart(0)
part.Proof.Aunts[0][0] += byte(0x01)
added, err := partSet2.AddPart(part)
if added || err == nil {
t.Errorf("expected to fail adding a part with bad trail.")
}
// Test adding a part with wrong bytes.
part = partSet.GetPart(1)
part.Bytes[0] += byte(0x01)
added, err = partSet2.AddPart(part)
if added || err == nil {
t.Errorf("expected to fail adding a part with bad bytes.")
}
}
func TestPartSetHeaderValidateBasic(t *testing.T) {
testCases := []struct {
testName string
malleatePartSetHeader func(*PartSetHeader)
expectErr bool
}{
{"Good PartSet", func(psHeader *PartSetHeader) {}, false},
{"Invalid Hash", func(psHeader *PartSetHeader) { psHeader.Hash = make([]byte, 1) }, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
data := tmrand.Bytes(testPartSize * 100)
ps := NewPartSetFromData(data, testPartSize)
psHeader := ps.Header()
tc.malleatePartSetHeader(&psHeader)
assert.Equal(t, tc.expectErr, psHeader.ValidateBasic() != nil, "Validate Basic had an unexpected result")
})
}
}
func TestPartValidateBasic(t *testing.T) {
testCases := []struct {
testName string
malleatePart func(*Part)
expectErr bool
}{
{"Good Part", func(pt *Part) {}, false},
{"Too big part", func(pt *Part) { pt.Bytes = make([]byte, BlockPartSizeBytes+1) }, true},
{"Too big proof", func(pt *Part) {
pt.Proof = merkle.Proof{
Total: 1,
Index: 1,
LeafHash: make([]byte, 1024*1024),
}
}, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
data := tmrand.Bytes(testPartSize * 100)
ps := NewPartSetFromData(data, testPartSize)
part := ps.GetPart(0)
tc.malleatePart(part)
assert.Equal(t, tc.expectErr, part.ValidateBasic() != nil, "Validate Basic had an unexpected result")
})
}
}
func TestParSetHeaderProtoBuf(t *testing.T) {
testCases := []struct {
msg string
ps1 *PartSetHeader
expPass bool
}{
{"success empty", &PartSetHeader{}, true},
{"success",
&PartSetHeader{Total: 1, Hash: []byte("hash")}, true},
}
for _, tc := range testCases {
protoBlockID := tc.ps1.ToProto()
psh, err := PartSetHeaderFromProto(&protoBlockID)
if tc.expPass {
require.Equal(t, tc.ps1, psh, tc.msg)
} else {
require.Error(t, err, tc.msg)
}
}
}
func TestPartProtoBuf(t *testing.T) {
proof := merkle.Proof{
Total: 1,
Index: 1,
LeafHash: tmrand.Bytes(32),
}
testCases := []struct {
msg string
ps1 *Part
expPass bool
}{
{"failure empty", &Part{}, false},
{"failure nil", nil, false},
{"success",
&Part{Index: 1, Bytes: tmrand.Bytes(32), Proof: proof}, true},
}
for _, tc := range testCases {
proto, err := tc.ps1.ToProto()
if tc.expPass {
require.NoError(t, err, tc.msg)
}
p, err := PartFromProto(proto)
if tc.expPass {
require.NoError(t, err)
require.Equal(t, tc.ps1, p, tc.msg)
}
}
}
+163
View File
@@ -0,0 +1,163 @@
package block
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// PrivValidatorType defines the implemtation types.
type PrivValidatorType uint8
const (
MockSignerClient = PrivValidatorType(0x00) // mock signer
FileSignerClient = PrivValidatorType(0x01) // signer client via file
RetrySignerClient = PrivValidatorType(0x02) // signer client with retry via socket
SignerSocketClient = PrivValidatorType(0x03) // signer client via socket
ErrorMockSignerClient = PrivValidatorType(0x04) // error mock signer
SignerGRPCClient = PrivValidatorType(0x05) // signer client via gRPC
)
// PrivValidator defines the functionality of a local Tendermint validator
// that signs votes and proposals, and never double signs.
type PrivValidator interface {
GetPubKey(context.Context) (crypto.PubKey, error)
SignVote(ctx context.Context, chainID string, vote *tmproto.Vote) error
SignProposal(ctx context.Context, chainID string, proposal *tmproto.Proposal) error
}
type PrivValidatorsByAddress []PrivValidator
func (pvs PrivValidatorsByAddress) Len() int {
return len(pvs)
}
func (pvs PrivValidatorsByAddress) Less(i, j int) bool {
pvi, err := pvs[i].GetPubKey(context.Background())
if err != nil {
panic(err)
}
pvj, err := pvs[j].GetPubKey(context.Background())
if err != nil {
panic(err)
}
return bytes.Compare(pvi.Address(), pvj.Address()) == -1
}
func (pvs PrivValidatorsByAddress) Swap(i, j int) {
pvs[i], pvs[j] = pvs[j], pvs[i]
}
//----------------------------------------
// MockPV
// MockPV implements PrivValidator without any safety or persistence.
// Only use it for testing.
type MockPV struct {
PrivKey crypto.PrivKey
breakProposalSigning bool
breakVoteSigning bool
}
func NewMockPV() MockPV {
return MockPV{ed25519.GenPrivKey(), false, false}
}
// NewMockPVWithParams allows one to create a MockPV instance, but with finer
// grained control over the operation of the mock validator. This is useful for
// mocking test failures.
func NewMockPVWithParams(privKey crypto.PrivKey, breakProposalSigning, breakVoteSigning bool) MockPV {
return MockPV{privKey, breakProposalSigning, breakVoteSigning}
}
// Implements PrivValidator.
func (pv MockPV) GetPubKey(ctx context.Context) (crypto.PubKey, error) {
return pv.PrivKey.PubKey(), nil
}
// Implements PrivValidator.
func (pv MockPV) SignVote(ctx context.Context, chainID string, vote *tmproto.Vote) error {
useChainID := chainID
if pv.breakVoteSigning {
useChainID = "incorrect-chain-id"
}
signBytes := VoteSignBytes(useChainID, vote)
sig, err := pv.PrivKey.Sign(signBytes)
if err != nil {
return err
}
vote.Signature = sig
return nil
}
// Implements PrivValidator.
func (pv MockPV) SignProposal(ctx context.Context, chainID string, proposal *tmproto.Proposal) error {
useChainID := chainID
if pv.breakProposalSigning {
useChainID = "incorrect-chain-id"
}
signBytes := ProposalSignBytes(useChainID, proposal)
sig, err := pv.PrivKey.Sign(signBytes)
if err != nil {
return err
}
proposal.Signature = sig
return nil
}
func (pv MockPV) ExtractIntoValidator(votingPower int64) *Validator {
pubKey, _ := pv.GetPubKey(context.Background())
return &Validator{
Address: pubKey.Address(),
PubKey: pubKey,
VotingPower: votingPower,
}
}
// String returns a string representation of the MockPV.
func (pv MockPV) String() string {
mpv, _ := pv.GetPubKey(context.Background()) // mockPV will never return an error, ignored here
return fmt.Sprintf("MockPV{%v}", mpv.Address())
}
// XXX: Implement.
func (pv MockPV) DisableChecks() {
// Currently this does nothing,
// as MockPV has no safety checks at all.
}
type ErroringMockPV struct {
MockPV
}
var ErroringMockPVErr = errors.New("erroringMockPV always returns an error")
// Implements PrivValidator.
func (pv *ErroringMockPV) GetPubKey(ctx context.Context) (crypto.PubKey, error) {
return nil, ErroringMockPVErr
}
// Implements PrivValidator.
func (pv *ErroringMockPV) SignVote(ctx context.Context, chainID string, vote *tmproto.Vote) error {
return ErroringMockPVErr
}
// Implements PrivValidator.
func (pv *ErroringMockPV) SignProposal(ctx context.Context, chainID string, proposal *tmproto.Proposal) error {
return ErroringMockPVErr
}
// NewErroringMockPV returns a MockPV that fails on each signing request. Again, for testing only.
func NewErroringMockPV() *ErroringMockPV {
return &ErroringMockPV{MockPV{ed25519.GenPrivKey(), false, false}}
}
+161
View File
@@ -0,0 +1,161 @@
package block
import (
"errors"
"fmt"
"time"
"github.com/tendermint/tendermint/internal/libs/protoio"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmtime "github.com/tendermint/tendermint/libs/time"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
var (
ErrInvalidBlockPartSignature = errors.New("error invalid block part signature")
ErrInvalidBlockPartHash = errors.New("error invalid block part hash")
)
// Proposal defines a block proposal for the consensus.
// It refers to the block by BlockID field.
// It must be signed by the correct proposer for the given Height/Round
// to be considered valid. It may depend on votes from a previous round,
// a so-called Proof-of-Lock (POL) round, as noted in the POLRound.
// If POLRound >= 0, then BlockID corresponds to the block that is locked in POLRound.
type Proposal struct {
Type tmproto.SignedMsgType
Height int64 `json:"height"`
Round int32 `json:"round"` // there can not be greater than 2_147_483_647 rounds
POLRound int32 `json:"pol_round"` // -1 if null.
BlockID BlockID `json:"block_id"`
Timestamp time.Time `json:"timestamp"`
Signature []byte `json:"signature"`
}
// NewProposal returns a new Proposal.
// If there is no POLRound, polRound should be -1.
func NewProposal(height int64, round int32, polRound int32, blockID BlockID) *Proposal {
return &Proposal{
Type: tmproto.ProposalType,
Height: height,
Round: round,
BlockID: blockID,
POLRound: polRound,
Timestamp: tmtime.Now(),
}
}
// ValidateBasic performs basic validation.
func (p *Proposal) ValidateBasic() error {
if p.Type != tmproto.ProposalType {
return errors.New("invalid Type")
}
if p.Height < 0 {
return errors.New("negative Height")
}
if p.Round < 0 {
return errors.New("negative Round")
}
if p.POLRound < -1 {
return errors.New("negative POLRound (exception: -1)")
}
if err := p.BlockID.ValidateBasic(); err != nil {
return fmt.Errorf("wrong BlockID: %v", err)
}
// ValidateBasic above would pass even if the BlockID was empty:
if !p.BlockID.IsComplete() {
return fmt.Errorf("expected a complete, non-empty BlockID, got: %v", p.BlockID)
}
// NOTE: Timestamp validation is subtle and handled elsewhere.
if len(p.Signature) == 0 {
return errors.New("signature is missing")
}
if len(p.Signature) > MaxSignatureSize {
return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize)
}
return nil
}
// String returns a string representation of the Proposal.
//
// 1. height
// 2. round
// 3. block ID
// 4. POL round
// 5. first 6 bytes of signature
// 6. timestamp
//
// See BlockID#String.
func (p *Proposal) String() string {
return fmt.Sprintf("Proposal{%v/%v (%v, %v) %X @ %s}",
p.Height,
p.Round,
p.BlockID,
p.POLRound,
tmbytes.Fingerprint(p.Signature),
CanonicalTime(p.Timestamp))
}
// ProposalSignBytes returns the proto-encoding of the canonicalized Proposal,
// for signing. Panics if the marshaling fails.
//
// The encoded Protobuf message is varint length-prefixed (using MarshalDelimited)
// for backwards-compatibility with the Amino encoding, due to e.g. hardware
// devices that rely on this encoding.
//
// See CanonicalizeProposal
func ProposalSignBytes(chainID string, p *tmproto.Proposal) []byte {
pb := CanonicalizeProposal(chainID, p)
bz, err := protoio.MarshalDelimited(&pb)
if err != nil {
panic(err)
}
return bz
}
// ToProto converts Proposal to protobuf
func (p *Proposal) ToProto() *tmproto.Proposal {
if p == nil {
return &tmproto.Proposal{}
}
pb := new(tmproto.Proposal)
pb.BlockID = p.BlockID.ToProto()
pb.Type = p.Type
pb.Height = p.Height
pb.Round = p.Round
pb.PolRound = p.POLRound
pb.Timestamp = p.Timestamp
pb.Signature = p.Signature
return pb
}
// FromProto sets a protobuf Proposal to the given pointer.
// It returns an error if the proposal is invalid.
func ProposalFromProto(pp *tmproto.Proposal) (*Proposal, error) {
if pp == nil {
return nil, errors.New("nil proposal")
}
p := new(Proposal)
blockID, err := BlockIDFromProto(&pp.BlockID)
if err != nil {
return nil, err
}
p.BlockID = *blockID
p.Type = pp.Type
p.Height = pp.Height
p.Round = pp.Round
p.POLRound = pp.PolRound
p.Timestamp = pp.Timestamp
p.Signature = pp.Signature
return p, p.ValidateBasic()
}
+193
View File
@@ -0,0 +1,193 @@
package block
import (
"context"
"math"
"testing"
"time"
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/internal/libs/protoio"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
var (
testProposal *Proposal
pbp *tmproto.Proposal
)
func init() {
var stamp, err = time.Parse(TimeFormat, "2018-02-11T07:09:22.765Z")
if err != nil {
panic(err)
}
testProposal = &Proposal{
Height: 12345,
Round: 23456,
BlockID: BlockID{Hash: []byte("--June_15_2020_amino_was_removed"),
PartSetHeader: PartSetHeader{Total: 111, Hash: []byte("--June_15_2020_amino_was_removed")}},
POLRound: -1,
Timestamp: stamp,
}
pbp = testProposal.ToProto()
}
func TestProposalSignable(t *testing.T) {
chainID := "test_chain_id"
signBytes := ProposalSignBytes(chainID, pbp)
pb := CanonicalizeProposal(chainID, pbp)
expected, err := protoio.MarshalDelimited(&pb)
require.NoError(t, err)
require.Equal(t, expected, signBytes, "Got unexpected sign bytes for Proposal")
}
func TestProposalString(t *testing.T) {
str := testProposal.String()
expected := `Proposal{12345/23456 (2D2D4A756E655F31355F323032305F616D696E6F5F7761735F72656D6F766564:111:2D2D4A756E65, -1) 000000000000 @ 2018-02-11T07:09:22.765Z}` //nolint:lll // ignore line length for tests
if str != expected {
t.Errorf("got unexpected string for Proposal. Expected:\n%v\nGot:\n%v", expected, str)
}
}
func TestProposalVerifySignature(t *testing.T) {
privVal := NewMockPV()
pubKey, err := privVal.GetPubKey(context.Background())
require.NoError(t, err)
prop := NewProposal(
4, 2, 2,
BlockID{tmrand.Bytes(tmhash.Size), PartSetHeader{777, tmrand.Bytes(tmhash.Size)}})
p := prop.ToProto()
signBytes := ProposalSignBytes("test_chain_id", p)
// sign it
err = privVal.SignProposal(context.Background(), "test_chain_id", p)
require.NoError(t, err)
prop.Signature = p.Signature
// verify the same proposal
valid := pubKey.VerifySignature(signBytes, prop.Signature)
require.True(t, valid)
// serialize, deserialize and verify again....
newProp := new(tmproto.Proposal)
pb := prop.ToProto()
bs, err := proto.Marshal(pb)
require.NoError(t, err)
err = proto.Unmarshal(bs, newProp)
require.NoError(t, err)
np, err := ProposalFromProto(newProp)
require.NoError(t, err)
// verify the transmitted proposal
newSignBytes := ProposalSignBytes("test_chain_id", pb)
require.Equal(t, string(signBytes), string(newSignBytes))
valid = pubKey.VerifySignature(newSignBytes, np.Signature)
require.True(t, valid)
}
func BenchmarkProposalWriteSignBytes(b *testing.B) {
for i := 0; i < b.N; i++ {
ProposalSignBytes("test_chain_id", pbp)
}
}
func BenchmarkProposalSign(b *testing.B) {
privVal := NewMockPV()
for i := 0; i < b.N; i++ {
err := privVal.SignProposal(context.Background(), "test_chain_id", pbp)
if err != nil {
b.Error(err)
}
}
}
func BenchmarkProposalVerifySignature(b *testing.B) {
privVal := NewMockPV()
err := privVal.SignProposal(context.Background(), "test_chain_id", pbp)
require.NoError(b, err)
pubKey, err := privVal.GetPubKey(context.Background())
require.NoError(b, err)
for i := 0; i < b.N; i++ {
pubKey.VerifySignature(ProposalSignBytes("test_chain_id", pbp), testProposal.Signature)
}
}
func TestProposalValidateBasic(t *testing.T) {
privVal := NewMockPV()
testCases := []struct {
testName string
malleateProposal func(*Proposal)
expectErr bool
}{
{"Good Proposal", func(p *Proposal) {}, false},
{"Invalid Type", func(p *Proposal) { p.Type = tmproto.PrecommitType }, true},
{"Invalid Height", func(p *Proposal) { p.Height = -1 }, true},
{"Invalid Round", func(p *Proposal) { p.Round = -1 }, true},
{"Invalid POLRound", func(p *Proposal) { p.POLRound = -2 }, true},
{"Invalid BlockId", func(p *Proposal) {
p.BlockID = BlockID{[]byte{1, 2, 3}, PartSetHeader{111, []byte("blockparts")}}
}, true},
{"Invalid Signature", func(p *Proposal) {
p.Signature = make([]byte, 0)
}, true},
{"Too big Signature", func(p *Proposal) {
p.Signature = make([]byte, MaxSignatureSize+1)
}, true},
}
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
prop := NewProposal(
4, 2, 2,
blockID)
p := prop.ToProto()
err := privVal.SignProposal(context.Background(), "test_chain_id", p)
prop.Signature = p.Signature
require.NoError(t, err)
tc.malleateProposal(prop)
assert.Equal(t, tc.expectErr, prop.ValidateBasic() != nil, "Validate Basic had an unexpected result")
})
}
}
func TestProposalProtoBuf(t *testing.T) {
proposal := NewProposal(1, 2, 3, makeBlockID([]byte("hash"), 2, []byte("part_set_hash")))
proposal.Signature = []byte("sig")
proposal2 := NewProposal(1, 2, 3, BlockID{})
testCases := []struct {
msg string
p1 *Proposal
expPass bool
}{
{"success", proposal, true},
{"success", proposal2, false}, // blcokID cannot be empty
{"empty proposal failure validatebasic", &Proposal{}, false},
{"nil proposal", nil, false},
}
for _, tc := range testCases {
protoProposal := tc.p1.ToProto()
p, err := ProposalFromProto(protoProposal)
if tc.expPass {
require.NoError(t, err)
require.Equal(t, tc.p1, p, tc.msg)
} else {
require.Error(t, err)
}
}
}
+62
View File
@@ -0,0 +1,62 @@
package block
import (
abci "github.com/tendermint/tendermint/abci/types"
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
)
//-------------------------------------------------------
// TM2PB is used for converting Tendermint ABCI to protobuf ABCI.
// UNSTABLE
var TM2PB = tm2pb{}
type tm2pb struct{}
func (tm2pb) Validator(val *Validator) abci.Validator {
return abci.Validator{
Address: val.PubKey.Address(),
Power: val.VotingPower,
}
}
// XXX: panics on unknown pubkey type
func (tm2pb) ValidatorUpdate(val *Validator) abci.ValidatorUpdate {
pk, err := cryptoenc.PubKeyToProto(val.PubKey)
if err != nil {
panic(err)
}
return abci.ValidatorUpdate{
PubKey: pk,
Power: val.VotingPower,
}
}
// XXX: panics on nil or unknown pubkey type
func (tm2pb) ValidatorUpdates(vals *ValidatorSet) []abci.ValidatorUpdate {
validators := make([]abci.ValidatorUpdate, vals.Size())
for i, val := range vals.Validators {
validators[i] = TM2PB.ValidatorUpdate(val)
}
return validators
}
//----------------------------------------------------------------------------
// PB2TM is used for converting protobuf ABCI to Tendermint ABCI.
// UNSTABLE
var PB2TM = pb2tm{}
type pb2tm struct{}
func (pb2tm) ValidatorUpdates(vals []abci.ValidatorUpdate) ([]*Validator, error) {
tmVals := make([]*Validator, len(vals))
for i, v := range vals {
pub, err := cryptoenc.PubKeyFromProto(v.PubKey)
if err != nil {
return nil, err
}
tmVals[i] = NewValidator(pub, v.Power)
}
return tmVals, nil
}
+67
View File
@@ -0,0 +1,67 @@
package block
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
)
func TestABCIPubKey(t *testing.T) {
pkEd := ed25519.GenPrivKey().PubKey()
err := testABCIPubKey(t, pkEd, ABCIPubKeyTypeEd25519)
assert.NoError(t, err)
}
func testABCIPubKey(t *testing.T, pk crypto.PubKey, typeStr string) error {
abciPubKey, err := cryptoenc.PubKeyToProto(pk)
require.NoError(t, err)
pk2, err := cryptoenc.PubKeyFromProto(abciPubKey)
require.NoError(t, err)
require.Equal(t, pk, pk2)
return nil
}
func TestABCIValidators(t *testing.T) {
pkEd := ed25519.GenPrivKey().PubKey()
// correct validator
tmValExpected := NewValidator(pkEd, 10)
tmVal := NewValidator(pkEd, 10)
abciVal := TM2PB.ValidatorUpdate(tmVal)
tmVals, err := PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{abciVal})
assert.Nil(t, err)
assert.Equal(t, tmValExpected, tmVals[0])
abciVals := TM2PB.ValidatorUpdates(NewValidatorSet(tmVals))
assert.Equal(t, []abci.ValidatorUpdate{abciVal}, abciVals)
// val with address
tmVal.Address = pkEd.Address()
abciVal = TM2PB.ValidatorUpdate(tmVal)
tmVals, err = PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{abciVal})
assert.Nil(t, err)
assert.Equal(t, tmValExpected, tmVals[0])
}
func TestABCIValidatorWithoutPubKey(t *testing.T) {
pkEd := ed25519.GenPrivKey().PubKey()
abciVal := TM2PB.Validator(NewValidator(pkEd, 10))
// pubkey must be nil
tmValExpected := abci.Validator{
Address: pkEd.Address(),
Power: 10,
}
assert.Equal(t, tmValExpected, abciVal)
}
+23
View File
@@ -0,0 +1,23 @@
package block
import (
"github.com/tendermint/tendermint/crypto/ed25519"
tmmath "github.com/tendermint/tendermint/libs/math"
)
var (
// MaxSignatureSize is a maximum allowed signature size for the Proposal
// and Vote.
// XXX: secp256k1 does not have Size nor MaxSize defined.
MaxSignatureSize = tmmath.MaxInt(ed25519.SignatureSize, 64)
)
// Signable is an interface for all signable things.
// It typically removes signatures before serializing.
// SignBytes returns the bytes to be signed
// NOTE: chainIDs are part of the SignBytes but not
// necessarily the object themselves.
// NOTE: Expected to panic if there is an error marshaling.
type Signable interface {
SignBytes(chainID string) []byte
}
+13
View File
@@ -0,0 +1,13 @@
package block
import tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
// IsVoteTypeValid returns true if t is a valid vote type.
func IsVoteTypeValid(t tmproto.SignedMsgType) bool {
switch t {
case tmproto.PrevoteType, tmproto.PrecommitType:
return true
default:
return false
}
}
+47
View File
@@ -0,0 +1,47 @@
package block
import (
"context"
"fmt"
"time"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
func makeCommit(blockID BlockID, height int64, round int32,
voteSet *VoteSet, validators []PrivValidator, now time.Time) (*Commit, error) {
// all sign
for i := 0; i < len(validators); i++ {
pubKey, err := validators[i].GetPubKey(context.Background())
if err != nil {
return nil, fmt.Errorf("can't get pubkey: %w", err)
}
vote := &Vote{
ValidatorAddress: pubKey.Address(),
ValidatorIndex: int32(i),
Height: height,
Round: round,
Type: tmproto.PrecommitType,
BlockID: blockID,
Timestamp: now,
}
_, err = signAddVote(validators[i], vote, voteSet)
if err != nil {
return nil, err
}
}
return voteSet.MakeCommit(), nil
}
func signAddVote(privVal PrivValidator, vote *Vote, voteSet *VoteSet) (signed bool, err error) {
v := vote.ToProto()
err = privVal.SignVote(context.Background(), voteSet.ChainID(), v)
if err != nil {
return false, err
}
vote.Signature = v.Signature
return voteSet.AddVote(vote)
}
+147
View File
@@ -0,0 +1,147 @@
package block
import (
"bytes"
"errors"
"fmt"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/crypto/tmhash"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// Tx is an arbitrary byte array.
// NOTE: Tx has no types at this level, so when wire encoded it's just length-prefixed.
// Might we want types here ?
type Tx []byte
// Hash computes the TMHASH hash of the wire encoded transaction.
func (tx Tx) Hash() []byte {
return tmhash.Sum(tx)
}
// String returns the hex-encoded transaction as a string.
func (tx Tx) String() string {
return fmt.Sprintf("Tx{%X}", []byte(tx))
}
// Txs is a slice of Tx.
type Txs []Tx
// Hash returns the Merkle root hash of the transaction hashes.
// i.e. the leaves of the tree are the hashes of the txs.
func (txs Txs) Hash() []byte {
// These allocations will be removed once Txs is switched to [][]byte,
// ref #2603. This is because golang does not allow type casting slices without unsafe
txBzs := make([][]byte, len(txs))
for i := 0; i < len(txs); i++ {
txBzs[i] = txs[i].Hash()
}
return merkle.HashFromByteSlices(txBzs)
}
// Index returns the index of this transaction in the list, or -1 if not found
func (txs Txs) Index(tx Tx) int {
for i := range txs {
if bytes.Equal(txs[i], tx) {
return i
}
}
return -1
}
// IndexByHash returns the index of this transaction hash in the list, or -1 if not found
func (txs Txs) IndexByHash(hash []byte) int {
for i := range txs {
if bytes.Equal(txs[i].Hash(), hash) {
return i
}
}
return -1
}
// Proof returns a simple merkle proof for this node.
// Panics if i < 0 or i >= len(txs)
// TODO: optimize this!
func (txs Txs) Proof(i int) TxProof {
l := len(txs)
bzs := make([][]byte, l)
for i := 0; i < l; i++ {
bzs[i] = txs[i].Hash()
}
root, proofs := merkle.ProofsFromByteSlices(bzs)
return TxProof{
RootHash: root,
Data: txs[i],
Proof: *proofs[i],
}
}
// TxProof represents a Merkle proof of the presence of a transaction in the Merkle tree.
type TxProof struct {
RootHash tmbytes.HexBytes `json:"root_hash"`
Data Tx `json:"data"`
Proof merkle.Proof `json:"proof"`
}
// Leaf returns the hash(tx), which is the leaf in the merkle tree which this proof refers to.
func (tp TxProof) Leaf() []byte {
return tp.Data.Hash()
}
// Validate verifies the proof. It returns nil if the RootHash matches the dataHash argument,
// and if the proof is internally consistent. Otherwise, it returns a sensible error.
func (tp TxProof) Validate(dataHash []byte) error {
if !bytes.Equal(dataHash, tp.RootHash) {
return errors.New("proof matches different data hash")
}
if tp.Proof.Index < 0 {
return errors.New("proof index cannot be negative")
}
if tp.Proof.Total <= 0 {
return errors.New("proof total must be positive")
}
valid := tp.Proof.Verify(tp.RootHash, tp.Leaf())
if valid != nil {
return errors.New("proof is not internally consistent")
}
return nil
}
func (tp TxProof) ToProto() tmproto.TxProof {
pbProof := tp.Proof.ToProto()
pbtp := tmproto.TxProof{
RootHash: tp.RootHash,
Data: tp.Data,
Proof: pbProof,
}
return pbtp
}
func TxProofFromProto(pb tmproto.TxProof) (TxProof, error) {
pbProof, err := merkle.ProofFromProto(pb.Proof)
if err != nil {
return TxProof{}, err
}
pbtp := TxProof{
RootHash: pb.RootHash,
Data: pb.Data,
Proof: *pbProof,
}
return pbtp, nil
}
// ComputeProtoSizeForTxs wraps the transactions in tmproto.Data{} and calculates the size.
// https://developers.google.com/protocol-buffers/docs/encoding
func ComputeProtoSizeForTxs(txs []Tx) int64 {
data := Data{Txs: txs}
pdData := data.ToProto()
return int64(pdData.Size())
}
+152
View File
@@ -0,0 +1,152 @@
package block
import (
"bytes"
mrand "math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ctest "github.com/tendermint/tendermint/internal/libs/test"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
func makeTxs(cnt, size int) Txs {
txs := make(Txs, cnt)
for i := 0; i < cnt; i++ {
txs[i] = tmrand.Bytes(size)
}
return txs
}
func randInt(low, high int) int {
off := mrand.Int() % (high - low)
return low + off
}
func TestTxIndex(t *testing.T) {
for i := 0; i < 20; i++ {
txs := makeTxs(15, 60)
for j := 0; j < len(txs); j++ {
tx := txs[j]
idx := txs.Index(tx)
assert.Equal(t, j, idx)
}
assert.Equal(t, -1, txs.Index(nil))
assert.Equal(t, -1, txs.Index(Tx("foodnwkf")))
}
}
func TestTxIndexByHash(t *testing.T) {
for i := 0; i < 20; i++ {
txs := makeTxs(15, 60)
for j := 0; j < len(txs); j++ {
tx := txs[j]
idx := txs.IndexByHash(tx.Hash())
assert.Equal(t, j, idx)
}
assert.Equal(t, -1, txs.IndexByHash(nil))
assert.Equal(t, -1, txs.IndexByHash(Tx("foodnwkf").Hash()))
}
}
func TestValidTxProof(t *testing.T) {
cases := []struct {
txs Txs
}{
{Txs{{1, 4, 34, 87, 163, 1}}},
{Txs{{5, 56, 165, 2}, {4, 77}}},
{Txs{Tx("foo"), Tx("bar"), Tx("baz")}},
{makeTxs(20, 5)},
{makeTxs(7, 81)},
{makeTxs(61, 15)},
}
for h, tc := range cases {
txs := tc.txs
root := txs.Hash()
// make sure valid proof for every tx
for i := range txs {
tx := []byte(txs[i])
proof := txs.Proof(i)
assert.EqualValues(t, i, proof.Proof.Index, "%d: %d", h, i)
assert.EqualValues(t, len(txs), proof.Proof.Total, "%d: %d", h, i)
assert.EqualValues(t, root, proof.RootHash, "%d: %d", h, i)
assert.EqualValues(t, tx, proof.Data, "%d: %d", h, i)
assert.EqualValues(t, txs[i].Hash(), proof.Leaf(), "%d: %d", h, i)
assert.Nil(t, proof.Validate(root), "%d: %d", h, i)
assert.NotNil(t, proof.Validate([]byte("foobar")), "%d: %d", h, i)
// read-write must also work
var (
p2 TxProof
pb2 tmproto.TxProof
)
pbProof := proof.ToProto()
bin, err := pbProof.Marshal()
require.NoError(t, err)
err = pb2.Unmarshal(bin)
require.NoError(t, err)
p2, err = TxProofFromProto(pb2)
if assert.Nil(t, err, "%d: %d: %+v", h, i, err) {
assert.Nil(t, p2.Validate(root), "%d: %d", h, i)
}
}
}
}
func TestTxProofUnchangable(t *testing.T) {
// run the other test a bunch...
for i := 0; i < 40; i++ {
testTxProofUnchangable(t)
}
}
func testTxProofUnchangable(t *testing.T) {
// make some proof
txs := makeTxs(randInt(2, 100), randInt(16, 128))
root := txs.Hash()
i := randInt(0, len(txs)-1)
proof := txs.Proof(i)
// make sure it is valid to start with
assert.Nil(t, proof.Validate(root))
pbProof := proof.ToProto()
bin, err := pbProof.Marshal()
require.NoError(t, err)
// try mutating the data and make sure nothing breaks
for j := 0; j < 500; j++ {
bad := ctest.MutateByteSlice(bin)
if !bytes.Equal(bad, bin) {
assertBadProof(t, root, bad, proof)
}
}
}
// This makes sure that the proof doesn't deserialize into something valid.
func assertBadProof(t *testing.T, root []byte, bad []byte, good TxProof) {
var (
proof TxProof
pbProof tmproto.TxProof
)
err := pbProof.Unmarshal(bad)
if err == nil {
proof, err = TxProofFromProto(pbProof)
if err == nil {
err = proof.Validate(root)
if err == nil {
// XXX Fix simple merkle proofs so the following is *not* OK.
// This can happen if we have a slightly different total (where the
// path ends up the same). If it is something else, we have a real
// problem.
assert.NotEqual(t, proof.Proof.Total, good.Proof.Total, "bad: %#v\ngood: %#v", proof, good)
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
package block
import "reflect"
// Go lacks a simple and safe way to see if something is a typed nil.
// See:
// - https://dave.cheney.net/2017/08/09/typed-nils-in-go-2
// - https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I/discussion
// - https://github.com/golang/go/issues/21538
func isTypedNil(o interface{}) bool {
rv := reflect.ValueOf(o)
switch rv.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
// Returns true if it has zero length.
func isEmpty(o interface{}) bool {
rv := reflect.ValueOf(o)
switch rv.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
return rv.Len() == 0
default:
return false
}
}
+357
View File
@@ -0,0 +1,357 @@
package block
import (
"errors"
"fmt"
"github.com/tendermint/tendermint/crypto/batch"
"github.com/tendermint/tendermint/crypto/tmhash"
tmmath "github.com/tendermint/tendermint/libs/math"
)
const batchVerifyThreshold = 2
func shouldBatchVerify(vals *ValidatorSet, commit *Commit) bool {
return len(commit.Signatures) >= batchVerifyThreshold && batch.SupportsBatchVerifier(vals.GetProposer().PubKey)
}
// VerifyCommit verifies +2/3 of the set had signed the given commit.
//
// It checks all the signatures! While it's safe to exit as soon as we have
// 2/3+ signatures, doing so would impact incentivization logic in the ABCI
// application that depends on the LastCommitInfo sent in BeginBlock, which
// includes which validators signed. For instance, Gaia incentivizes proposers
// with a bonus for including more than +2/3 of the signatures.
func VerifyCommit(chainID string, vals *ValidatorSet, blockID BlockID,
height int64, commit *Commit) error {
// run a basic validation of the arguments
if err := verifyBasicValsAndCommit(vals, commit, height, blockID); err != nil {
return err
}
// calculate voting power needed. Note that total voting power is capped to
// 1/8th of max int64 so this operation should never overflow
votingPowerNeeded := vals.TotalVotingPower() * 2 / 3
// ignore all absent signatures
ignore := func(c CommitSig) bool { return c.Absent() }
// only count the signatures that are for the block
count := func(c CommitSig) bool { return c.ForBlock() }
// attempt to batch verify
if shouldBatchVerify(vals, commit) {
return verifyCommitBatch(chainID, vals, commit,
votingPowerNeeded, ignore, count, true, true)
}
// if verification failed or is not supported then fallback to single verification
return verifyCommitSingle(chainID, vals, commit, votingPowerNeeded,
ignore, count, true, true)
}
// LIGHT CLIENT VERIFICATION METHODS
// VerifyCommitLight verifies +2/3 of the set had signed the given commit.
//
// This method is primarily used by the light client and does not check all the
// signatures.
func VerifyCommitLight(chainID string, vals *ValidatorSet, blockID BlockID,
height int64, commit *Commit) error {
// run a basic validation of the arguments
if err := verifyBasicValsAndCommit(vals, commit, height, blockID); err != nil {
return err
}
// calculate voting power needed
votingPowerNeeded := vals.TotalVotingPower() * 2 / 3
// ignore all commit signatures that are not for the block
ignore := func(c CommitSig) bool { return !c.ForBlock() }
// count all the remaining signatures
count := func(c CommitSig) bool { return true }
// attempt to batch verify
if shouldBatchVerify(vals, commit) {
return verifyCommitBatch(chainID, vals, commit,
votingPowerNeeded, ignore, count, false, true)
}
// if verification failed or is not supported then fallback to single verification
return verifyCommitSingle(chainID, vals, commit, votingPowerNeeded,
ignore, count, false, true)
}
// VerifyCommitLightTrusting verifies that trustLevel of the validator set signed
// this commit.
//
// NOTE the given validators do not necessarily correspond to the validator set
// for this commit, but there may be some intersection.
//
// This method is primarily used by the light client and does not check all the
// signatures.
func VerifyCommitLightTrusting(chainID string, vals *ValidatorSet, commit *Commit, trustLevel tmmath.Fraction) error {
// sanity checks
if vals == nil {
return errors.New("nil validator set")
}
if trustLevel.Denominator == 0 {
return errors.New("trustLevel has zero Denominator")
}
if commit == nil {
return errors.New("nil commit")
}
// safely calculate voting power needed.
totalVotingPowerMulByNumerator, overflow := safeMul(vals.TotalVotingPower(), int64(trustLevel.Numerator))
if overflow {
return errors.New("int64 overflow while calculating voting power needed. please provide smaller trustLevel numerator")
}
votingPowerNeeded := totalVotingPowerMulByNumerator / int64(trustLevel.Denominator)
// ignore all commit signatures that are not for the block
ignore := func(c CommitSig) bool { return !c.ForBlock() }
// count all the remaining signatures
count := func(c CommitSig) bool { return true }
// attempt to batch verify commit. As the validator set doesn't necessarily
// correspond with the validator set that signed the block we need to look
// up by address rather than index.
if shouldBatchVerify(vals, commit) {
return verifyCommitBatch(chainID, vals, commit,
votingPowerNeeded, ignore, count, false, false)
}
// attempt with single verification
return verifyCommitSingle(chainID, vals, commit, votingPowerNeeded,
ignore, count, false, false)
}
// ValidateHash returns an error if the hash is not empty, but its
// size != tmhash.Size.
func ValidateHash(h []byte) error {
if len(h) > 0 && len(h) != tmhash.Size {
return fmt.Errorf("expected size to be %d bytes, got %d bytes",
tmhash.Size,
len(h),
)
}
return nil
}
// Batch verification
// verifyCommitBatch batch verifies commits. This routine is equivalent
// to verifyCommitSingle in behavior, just faster iff every signature in the
// batch is valid.
//
// Note: The caller is responsible for checking to see if this routine is
// usable via `shouldVerifyBatch(vals, commit)`.
func verifyCommitBatch(
chainID string,
vals *ValidatorSet,
commit *Commit,
votingPowerNeeded int64,
ignoreSig func(CommitSig) bool,
countSig func(CommitSig) bool,
countAllSignatures bool,
lookUpByIndex bool,
) error {
var (
val *Validator
valIdx int32
seenVals = make(map[int32]int, len(commit.Signatures))
batchSigIdxs = make([]int, 0, len(commit.Signatures))
talliedVotingPower int64 = 0
)
// attempt to create a batch verifier
bv, ok := batch.CreateBatchVerifier(vals.GetProposer().PubKey)
// re-check if batch verification is supported
if !ok || len(commit.Signatures) < batchVerifyThreshold {
// This should *NEVER* happen.
return fmt.Errorf("unsupported signature algorithm or insufficient signatures for batch verification")
}
for idx, commitSig := range commit.Signatures {
// skip over signatures that should be ignored
if ignoreSig(commitSig) {
continue
}
// If the vals and commit have a 1-to-1 correspondance we can retrieve
// them by index else we need to retrieve them by address
if lookUpByIndex {
val = vals.Validators[idx]
} else {
valIdx, val = vals.GetByAddress(commitSig.ValidatorAddress)
// if the signature doesn't belong to anyone in the validator set
// then we just skip over it
if val == nil {
continue
}
// because we are getting validators by address we need to make sure
// that the same validator doesn't commit twice
if firstIndex, ok := seenVals[valIdx]; ok {
secondIndex := idx
return fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
}
seenVals[valIdx] = idx
}
// Validate signature.
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
// add the key, sig and message to the verifier
if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
return err
}
batchSigIdxs = append(batchSigIdxs, idx)
// If this signature counts then add the voting power of the validator
// to the tally
if countSig(commitSig) {
talliedVotingPower += val.VotingPower
}
// if we don't need to verify all signatures and already have sufficient
// voting power we can break from batching and verify all the signatures
if !countAllSignatures && talliedVotingPower > votingPowerNeeded {
break
}
}
// ensure that we have batched together enough signatures to exceed the
// voting power needed else there is no need to even verify
if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed {
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
}
// attempt to verify the batch.
ok, validSigs := bv.Verify()
if ok {
// success
return nil
}
// one or more of the signatures is invalid, find and return the first
// invalid signature.
for i, ok := range validSigs {
if !ok {
// go back from the batch index to the commit.Signatures index
idx := batchSigIdxs[i]
sig := commit.Signatures[idx]
return fmt.Errorf("wrong signature (#%d): %X", idx, sig)
}
}
// execution reaching here is a bug, and one of the following has
// happened:
// * non-zero tallied voting power, empty batch (impossible?)
// * bv.Verify() returned `false, []bool{true, ..., true}` (BUG)
return fmt.Errorf("BUG: batch verification failed with no invalid signatures")
}
// Single Verification
// verifyCommitSingle single verifies commits.
// If a key does not support batch verification, or batch verification fails this will be used
// This method is used to check all the signatures included in a commit.
// It is used in consensus for validating a block LastCommit.
// CONTRACT: both commit and validator set should have passed validate basic
func verifyCommitSingle(
chainID string,
vals *ValidatorSet,
commit *Commit,
votingPowerNeeded int64,
ignoreSig func(CommitSig) bool,
countSig func(CommitSig) bool,
countAllSignatures bool,
lookUpByIndex bool,
) error {
var (
val *Validator
valIdx int32
seenVals = make(map[int32]int, len(commit.Signatures))
talliedVotingPower int64 = 0
voteSignBytes []byte
)
for idx, commitSig := range commit.Signatures {
if ignoreSig(commitSig) {
continue
}
// If the vals and commit have a 1-to-1 correspondance we can retrieve
// them by index else we need to retrieve them by address
if lookUpByIndex {
val = vals.Validators[idx]
} else {
valIdx, val = vals.GetByAddress(commitSig.ValidatorAddress)
// if the signature doesn't belong to anyone in the validator set
// then we just skip over it
if val == nil {
continue
}
// because we are getting validators by address we need to make sure
// that the same validator doesn't commit twice
if firstIndex, ok := seenVals[valIdx]; ok {
secondIndex := idx
return fmt.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex)
}
seenVals[valIdx] = idx
}
voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))
if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)
}
// If this signature counts then add the voting power of the validator
// to the tally
if countSig(commitSig) {
talliedVotingPower += val.VotingPower
}
// check if we have enough signatures and can thus exit early
if !countAllSignatures && talliedVotingPower > votingPowerNeeded {
return nil
}
}
if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed {
return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed}
}
return nil
}
func verifyBasicValsAndCommit(vals *ValidatorSet, commit *Commit, height int64, blockID BlockID) error {
if vals == nil {
return errors.New("nil validator set")
}
if commit == nil {
return errors.New("nil commit")
}
if vals.Size() != len(commit.Signatures) {
return NewErrInvalidCommitSignatures(vals.Size(), len(commit.Signatures))
}
// Validate Height and BlockID.
if height != commit.Height {
return NewErrInvalidCommitHeight(height, commit.Height)
}
if !blockID.Equals(commit.BlockID) {
return fmt.Errorf("invalid commit -- wrong block ID: want %v, got %v",
blockID, commit.BlockID)
}
return nil
}
+262
View File
@@ -0,0 +1,262 @@
package block
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tmmath "github.com/tendermint/tendermint/libs/math"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// Check VerifyCommit, VerifyCommitLight and VerifyCommitLightTrusting basic
// verification.
func TestValidatorSet_VerifyCommit_All(t *testing.T) {
var (
round = int32(0)
height = int64(100)
blockID = makeBlockID([]byte("blockhash"), 1000, []byte("partshash"))
chainID = "Lalande21185"
trustLevel = tmmath.Fraction{Numerator: 2, Denominator: 3}
)
testCases := []struct {
description string
// vote chainID
chainID string
// vote blockID
blockID BlockID
valSize int
// height of the commit
height int64
// votes
blockVotes int
nilVotes int
absentVotes int
expErr bool
}{
{"good (batch verification)", chainID, blockID, 3, height, 3, 0, 0, false},
{"good (single verification)", chainID, blockID, 1, height, 1, 0, 0, false},
{"wrong signature (#0)", "EpsilonEridani", blockID, 2, height, 2, 0, 0, true},
{"wrong block ID", chainID, makeBlockIDRandom(), 2, height, 2, 0, 0, true},
{"wrong height", chainID, blockID, 1, height - 1, 1, 0, 0, true},
{"wrong set size: 4 vs 3", chainID, blockID, 4, height, 3, 0, 0, true},
{"wrong set size: 1 vs 2", chainID, blockID, 1, height, 2, 0, 0, true},
{"insufficient voting power: got 30, needed more than 66", chainID, blockID, 10, height, 3, 2, 5, true},
{"insufficient voting power: got 0, needed more than 6", chainID, blockID, 1, height, 0, 0, 1, true},
{"insufficient voting power: got 60, needed more than 60", chainID, blockID, 9, height, 6, 3, 0, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) {
_, valSet, vals := randVoteSet(tc.height, round, tmproto.PrecommitType, tc.valSize, 10)
totalVotes := tc.blockVotes + tc.absentVotes + tc.nilVotes
sigs := make([]CommitSig, totalVotes)
vi := 0
// add absent sigs first
for i := 0; i < tc.absentVotes; i++ {
sigs[vi] = NewCommitSigAbsent()
vi++
}
for i := 0; i < tc.blockVotes+tc.nilVotes; i++ {
pubKey, err := vals[vi%len(vals)].GetPubKey(context.Background())
require.NoError(t, err)
vote := &Vote{
ValidatorAddress: pubKey.Address(),
ValidatorIndex: int32(vi),
Height: tc.height,
Round: round,
Type: tmproto.PrecommitType,
BlockID: tc.blockID,
Timestamp: time.Now(),
}
if i >= tc.blockVotes {
vote.BlockID = BlockID{}
}
v := vote.ToProto()
require.NoError(t, vals[vi%len(vals)].SignVote(context.Background(), tc.chainID, v))
vote.Signature = v.Signature
sigs[vi] = vote.CommitSig()
vi++
}
commit := NewCommit(tc.height, round, tc.blockID, sigs)
err := valSet.VerifyCommit(chainID, blockID, height, commit)
if tc.expErr {
if assert.Error(t, err, "VerifyCommit") {
assert.Contains(t, err.Error(), tc.description, "VerifyCommit")
}
} else {
assert.NoError(t, err, "VerifyCommit")
}
err = valSet.VerifyCommitLight(chainID, blockID, height, commit)
if tc.expErr {
if assert.Error(t, err, "VerifyCommitLight") {
assert.Contains(t, err.Error(), tc.description, "VerifyCommitLight")
}
} else {
assert.NoError(t, err, "VerifyCommitLight")
}
// only a subsection of the tests apply to VerifyCommitLightTrusting
if totalVotes != tc.valSize || !tc.blockID.Equals(blockID) || tc.height != height {
tc.expErr = false
}
err = valSet.VerifyCommitLightTrusting(chainID, commit, trustLevel)
if tc.expErr {
if assert.Error(t, err, "VerifyCommitLightTrusting") {
assert.Contains(t, err.Error(), tc.description, "VerifyCommitLightTrusting")
}
} else {
assert.NoError(t, err, "VerifyCommitLightTrusting")
}
})
}
}
func TestValidatorSet_VerifyCommit_CheckAllSignatures(t *testing.T) {
var (
chainID = "test_chain_id"
h = int64(3)
blockID = makeBlockIDRandom()
)
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
commit, err := makeCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
// malleate 4th signature
vote := voteSet.GetByIndex(3)
v := vote.ToProto()
err = vals[3].SignVote(context.Background(), "CentaurusA", v)
require.NoError(t, err)
vote.Signature = v.Signature
commit.Signatures[3] = vote.CommitSig()
err = valSet.VerifyCommit(chainID, blockID, h, commit)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "wrong signature (#3)")
}
}
func TestValidatorSet_VerifyCommitLight_ReturnsAsSoonAsMajorityOfVotingPowerSigned(t *testing.T) {
var (
chainID = "test_chain_id"
h = int64(3)
blockID = makeBlockIDRandom()
)
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
commit, err := makeCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
// malleate 4th signature (3 signatures are enough for 2/3+)
vote := voteSet.GetByIndex(3)
v := vote.ToProto()
err = vals[3].SignVote(context.Background(), "CentaurusA", v)
require.NoError(t, err)
vote.Signature = v.Signature
commit.Signatures[3] = vote.CommitSig()
err = valSet.VerifyCommitLight(chainID, blockID, h, commit)
assert.NoError(t, err)
}
func TestValidatorSet_VerifyCommitLightTrusting_ReturnsAsSoonAsTrustLevelOfVotingPowerSigned(t *testing.T) {
var (
chainID = "test_chain_id"
h = int64(3)
blockID = makeBlockIDRandom()
)
voteSet, valSet, vals := randVoteSet(h, 0, tmproto.PrecommitType, 4, 10)
commit, err := makeCommit(blockID, h, 0, voteSet, vals, time.Now())
require.NoError(t, err)
require.NoError(t, valSet.VerifyCommit(chainID, blockID, h, commit))
// malleate 3rd signature (2 signatures are enough for 1/3+ trust level)
vote := voteSet.GetByIndex(2)
v := vote.ToProto()
err = vals[2].SignVote(context.Background(), "CentaurusA", v)
require.NoError(t, err)
vote.Signature = v.Signature
commit.Signatures[2] = vote.CommitSig()
err = valSet.VerifyCommitLightTrusting(chainID, commit, tmmath.Fraction{Numerator: 1, Denominator: 3})
assert.NoError(t, err)
}
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())
newValSet, _ = randValidatorPrivValSet(2, 1)
)
require.NoError(t, err)
testCases := []struct {
valSet *ValidatorSet
err bool
}{
// good
0: {
valSet: originalValset,
err: false,
},
// bad - no overlap between validator sets
1: {
valSet: newValSet,
err: true,
},
// good - first two are different but the rest of the same -> >1/3
2: {
valSet: NewValidatorSet(append(newValSet.Validators, originalValset.Validators...)),
err: false,
},
}
for _, tc := range testCases {
err = tc.valSet.VerifyCommitLightTrusting("test_chain_id", commit,
tmmath.Fraction{Numerator: 1, Denominator: 3})
if tc.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
}
}
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())
)
require.NoError(t, err)
err = valSet.VerifyCommitLightTrusting("test_chain_id", commit,
tmmath.Fraction{Numerator: 25, Denominator: 55})
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "int64 overflow")
}
}
+173
View File
@@ -0,0 +1,173 @@
package block
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/tendermint/tendermint/crypto"
ce "github.com/tendermint/tendermint/crypto/encoding"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
// Volatile state for each Validator
// NOTE: The ProposerPriority is not included in Validator.Hash();
// make sure to update that method if changes are made here
type Validator struct {
Address Address `json:"address"`
PubKey crypto.PubKey `json:"pub_key"`
VotingPower int64 `json:"voting_power"`
ProposerPriority int64 `json:"proposer_priority"`
}
// NewValidator returns a new validator with the given pubkey and voting power.
func NewValidator(pubKey crypto.PubKey, votingPower int64) *Validator {
return &Validator{
Address: pubKey.Address(),
PubKey: pubKey,
VotingPower: votingPower,
ProposerPriority: 0,
}
}
// ValidateBasic performs basic validation.
func (v *Validator) ValidateBasic() error {
if v == nil {
return errors.New("nil validator")
}
if v.PubKey == nil {
return errors.New("validator does not have a public key")
}
if v.VotingPower < 0 {
return errors.New("validator has negative voting power")
}
if len(v.Address) != crypto.AddressSize {
return fmt.Errorf("validator address is the wrong size: %v", v.Address)
}
return nil
}
// Creates a new copy of the validator so we can mutate ProposerPriority.
// Panics if the validator is nil.
func (v *Validator) Copy() *Validator {
vCopy := *v
return &vCopy
}
// Returns the one with higher ProposerPriority.
func (v *Validator) CompareProposerPriority(other *Validator) *Validator {
if v == nil {
return other
}
switch {
case v.ProposerPriority > other.ProposerPriority:
return v
case v.ProposerPriority < other.ProposerPriority:
return other
default:
result := bytes.Compare(v.Address, other.Address)
switch {
case result < 0:
return v
case result > 0:
return other
default:
panic("Cannot compare identical validators")
}
}
}
// String returns a string representation of String.
//
// 1. address
// 2. public key
// 3. voting power
// 4. proposer priority
func (v *Validator) String() string {
if v == nil {
return "nil-Validator"
}
return fmt.Sprintf("Validator{%v %v VP:%v A:%v}",
v.Address,
v.PubKey,
v.VotingPower,
v.ProposerPriority)
}
// ValidatorListString returns a prettified validator list for logging purposes.
func ValidatorListString(vals []*Validator) string {
chunks := make([]string, len(vals))
for i, val := range vals {
chunks[i] = fmt.Sprintf("%s:%d", val.Address, val.VotingPower)
}
return strings.Join(chunks, ",")
}
// Bytes computes the unique encoding of a validator with a given voting power.
// These are the bytes that gets hashed in consensus. It excludes address
// as its redundant with the pubkey. This also excludes ProposerPriority
// which changes every round.
func (v *Validator) Bytes() []byte {
pk, err := ce.PubKeyToProto(v.PubKey)
if err != nil {
panic(err)
}
pbv := tmproto.SimpleValidator{
PubKey: &pk,
VotingPower: v.VotingPower,
}
bz, err := pbv.Marshal()
if err != nil {
panic(err)
}
return bz
}
// ToProto converts Valiator to protobuf
func (v *Validator) ToProto() (*tmproto.Validator, error) {
if v == nil {
return nil, errors.New("nil validator")
}
pk, err := ce.PubKeyToProto(v.PubKey)
if err != nil {
return nil, err
}
vp := tmproto.Validator{
Address: v.Address,
PubKey: pk,
VotingPower: v.VotingPower,
ProposerPriority: v.ProposerPriority,
}
return &vp, nil
}
// FromProto sets a protobuf Validator to the given pointer.
// It returns an error if the public key is invalid.
func ValidatorFromProto(vp *tmproto.Validator) (*Validator, error) {
if vp == nil {
return nil, errors.New("nil validator")
}
pk, err := ce.PubKeyFromProto(vp.PubKey)
if err != nil {
return nil, err
}
v := new(Validator)
v.Address = vp.GetAddress()
v.PubKey = pk
v.VotingPower = vp.GetVotingPower()
v.ProposerPriority = vp.GetProposerPriority()
return v, nil
}
+933
View File
@@ -0,0 +1,933 @@
package block
import (
"bytes"
"errors"
"fmt"
"math"
"math/big"
"sort"
"strings"
"github.com/tendermint/tendermint/crypto/merkle"
tmmath "github.com/tendermint/tendermint/libs/math"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
const (
// MaxTotalVotingPower - the maximum allowed total voting power.
// It needs to be sufficiently small to, in all cases:
// 1. prevent clipping in incrementProposerPriority()
// 2. let (diff+diffMax-1) not overflow in IncrementProposerPriority()
// (Proof of 1 is tricky, left to the reader).
// It could be higher, but this is sufficiently large for our purposes,
// and leaves room for defensive purposes.
MaxTotalVotingPower = int64(math.MaxInt64) / 8
// PriorityWindowSizeFactor - is a constant that when multiplied with the
// total voting power gives the maximum allowed distance between validator
// priorities.
PriorityWindowSizeFactor = 2
)
// ErrTotalVotingPowerOverflow is returned if the total voting power of the
// resulting validator set exceeds MaxTotalVotingPower.
var ErrTotalVotingPowerOverflow = fmt.Errorf("total voting power of resulting valset exceeds max %d",
MaxTotalVotingPower)
// ValidatorSet represent a set of *Validator at a given height.
//
// The validators can be fetched by address or index.
// The index is in order of .VotingPower, so the indices are fixed for all
// rounds of a given blockchain height - ie. the validators are sorted by their
// voting power (descending). Secondary index - .Address (ascending).
//
// On the other hand, the .ProposerPriority of each validator and the
// designated .GetProposer() of a set changes every round, upon calling
// .IncrementProposerPriority().
//
// NOTE: Not goroutine-safe.
// NOTE: All get/set to validators should copy the value for safety.
type ValidatorSet struct {
// NOTE: persisted via reflect, must be exported.
Validators []*Validator `json:"validators"`
Proposer *Validator `json:"proposer"`
// cached (unexported)
totalVotingPower int64
}
// NewValidatorSet initializes a ValidatorSet by copying over the values from
// `valz`, a list of Validators. If valz is nil or empty, the new ValidatorSet
// will have an empty list of Validators.
//
// The addresses of validators in `valz` must be unique otherwise the function
// panics.
//
// Note the validator set size has an implied limit equal to that of the
// MaxVotesCount - commits by a validator set larger than this will fail
// validation.
func NewValidatorSet(valz []*Validator) *ValidatorSet {
vals := &ValidatorSet{}
err := vals.updateWithChangeSet(valz, false)
if err != nil {
panic(fmt.Sprintf("Cannot create validator set: %v", err))
}
if len(valz) > 0 {
vals.IncrementProposerPriority(1)
}
return vals
}
func (vals *ValidatorSet) ValidateBasic() error {
if vals.IsNilOrEmpty() {
return errors.New("validator set is nil or empty")
}
for idx, val := range vals.Validators {
if err := val.ValidateBasic(); err != nil {
return fmt.Errorf("invalid validator #%d: %w", idx, err)
}
}
if err := vals.Proposer.ValidateBasic(); err != nil {
return fmt.Errorf("proposer failed validate basic, error: %w", err)
}
return nil
}
// IsNilOrEmpty returns true if validator set is nil or empty.
func (vals *ValidatorSet) IsNilOrEmpty() bool {
return vals == nil || len(vals.Validators) == 0
}
// CopyIncrementProposerPriority increments ProposerPriority and updates the
// proposer on a copy, and returns it.
func (vals *ValidatorSet) CopyIncrementProposerPriority(times int32) *ValidatorSet {
copy := vals.Copy()
copy.IncrementProposerPriority(times)
return copy
}
// IncrementProposerPriority increments ProposerPriority of each validator and
// updates the proposer. Panics if validator set is empty.
// `times` must be positive.
func (vals *ValidatorSet) IncrementProposerPriority(times int32) {
if vals.IsNilOrEmpty() {
panic("empty validator set")
}
if times <= 0 {
panic("Cannot call IncrementProposerPriority with non-positive times")
}
// Cap the difference between priorities to be proportional to 2*totalPower by
// re-normalizing priorities, i.e., rescale all priorities by multiplying with:
// 2*totalVotingPower/(maxPriority - minPriority)
diffMax := PriorityWindowSizeFactor * vals.TotalVotingPower()
vals.RescalePriorities(diffMax)
vals.shiftByAvgProposerPriority()
var proposer *Validator
// Call IncrementProposerPriority(1) times times.
for i := int32(0); i < times; i++ {
proposer = vals.incrementProposerPriority()
}
vals.Proposer = proposer
}
// RescalePriorities rescales the priorities such that the distance between the
// maximum and minimum is smaller than `diffMax`. Panics if validator set is
// empty.
func (vals *ValidatorSet) RescalePriorities(diffMax int64) {
if vals.IsNilOrEmpty() {
panic("empty validator set")
}
// NOTE: This check is merely a sanity check which could be
// removed if all tests would init. voting power appropriately;
// i.e. diffMax should always be > 0
if diffMax <= 0 {
return
}
// Calculating ceil(diff/diffMax):
// Re-normalization is performed by dividing by an integer for simplicity.
// NOTE: This may make debugging priority issues easier as well.
diff := computeMaxMinPriorityDiff(vals)
ratio := (diff + diffMax - 1) / diffMax
if diff > diffMax {
for _, val := range vals.Validators {
val.ProposerPriority /= ratio
}
}
}
func (vals *ValidatorSet) incrementProposerPriority() *Validator {
for _, val := range vals.Validators {
// Check for overflow for sum.
newPrio := safeAddClip(val.ProposerPriority, val.VotingPower)
val.ProposerPriority = newPrio
}
// Decrement the validator with most ProposerPriority.
mostest := vals.getValWithMostPriority()
// Mind the underflow.
mostest.ProposerPriority = safeSubClip(mostest.ProposerPriority, vals.TotalVotingPower())
return mostest
}
// Should not be called on an empty validator set.
func (vals *ValidatorSet) computeAvgProposerPriority() int64 {
n := int64(len(vals.Validators))
sum := big.NewInt(0)
for _, val := range vals.Validators {
sum.Add(sum, big.NewInt(val.ProposerPriority))
}
avg := sum.Div(sum, big.NewInt(n))
if avg.IsInt64() {
return avg.Int64()
}
// This should never happen: each val.ProposerPriority is in bounds of int64.
panic(fmt.Sprintf("Cannot represent avg ProposerPriority as an int64 %v", avg))
}
// Compute the difference between the max and min ProposerPriority of that set.
func computeMaxMinPriorityDiff(vals *ValidatorSet) int64 {
if vals.IsNilOrEmpty() {
panic("empty validator set")
}
max := int64(math.MinInt64)
min := int64(math.MaxInt64)
for _, v := range vals.Validators {
if v.ProposerPriority < min {
min = v.ProposerPriority
}
if v.ProposerPriority > max {
max = v.ProposerPriority
}
}
diff := max - min
if diff < 0 {
return -1 * diff
}
return diff
}
func (vals *ValidatorSet) getValWithMostPriority() *Validator {
var res *Validator
for _, val := range vals.Validators {
res = res.CompareProposerPriority(val)
}
return res
}
func (vals *ValidatorSet) shiftByAvgProposerPriority() {
if vals.IsNilOrEmpty() {
panic("empty validator set")
}
avgProposerPriority := vals.computeAvgProposerPriority()
for _, val := range vals.Validators {
val.ProposerPriority = safeSubClip(val.ProposerPriority, avgProposerPriority)
}
}
// Makes a copy of the validator list.
func validatorListCopy(valsList []*Validator) []*Validator {
if valsList == nil {
return nil
}
valsCopy := make([]*Validator, len(valsList))
for i, val := range valsList {
valsCopy[i] = val.Copy()
}
return valsCopy
}
// Copy each validator into a new ValidatorSet.
func (vals *ValidatorSet) Copy() *ValidatorSet {
return &ValidatorSet{
Validators: validatorListCopy(vals.Validators),
Proposer: vals.Proposer,
totalVotingPower: vals.totalVotingPower,
}
}
// HasAddress returns true if address given is in the validator set, false -
// otherwise.
func (vals *ValidatorSet) HasAddress(address []byte) bool {
for _, val := range vals.Validators {
if bytes.Equal(val.Address, address) {
return true
}
}
return false
}
// GetByAddress returns an index of the validator with address and validator
// itself (copy) if found. Otherwise, -1 and nil are returned.
func (vals *ValidatorSet) GetByAddress(address []byte) (index int32, val *Validator) {
for idx, val := range vals.Validators {
if bytes.Equal(val.Address, address) {
return int32(idx), val.Copy()
}
}
return -1, nil
}
// GetByIndex returns the validator's address and validator itself (copy) by
// index.
// It returns nil values if index is less than 0 or greater or equal to
// len(ValidatorSet.Validators).
func (vals *ValidatorSet) GetByIndex(index int32) (address []byte, val *Validator) {
if index < 0 || int(index) >= len(vals.Validators) {
return nil, nil
}
val = vals.Validators[index]
return val.Address, val.Copy()
}
// Size returns the length of the validator set.
func (vals *ValidatorSet) Size() int {
return len(vals.Validators)
}
// Forces recalculation of the set's total voting power.
// Panics if total voting power is bigger than MaxTotalVotingPower.
func (vals *ValidatorSet) updateTotalVotingPower() {
sum := int64(0)
for _, val := range vals.Validators {
// mind overflow
sum = safeAddClip(sum, val.VotingPower)
if sum > MaxTotalVotingPower {
panic(fmt.Sprintf(
"Total voting power should be guarded to not exceed %v; got: %v",
MaxTotalVotingPower,
sum))
}
}
vals.totalVotingPower = sum
}
// TotalVotingPower returns the sum of the voting powers of all validators.
// It recomputes the total voting power if required.
func (vals *ValidatorSet) TotalVotingPower() int64 {
if vals.totalVotingPower == 0 {
vals.updateTotalVotingPower()
}
return vals.totalVotingPower
}
// GetProposer returns the current proposer. If the validator set is empty, nil
// is returned.
func (vals *ValidatorSet) GetProposer() (proposer *Validator) {
if len(vals.Validators) == 0 {
return nil
}
if vals.Proposer == nil {
vals.Proposer = vals.findProposer()
}
return vals.Proposer.Copy()
}
func (vals *ValidatorSet) findProposer() *Validator {
var proposer *Validator
for _, val := range vals.Validators {
if proposer == nil || !bytes.Equal(val.Address, proposer.Address) {
proposer = proposer.CompareProposerPriority(val)
}
}
return proposer
}
// Hash returns the Merkle root hash build using validators (as leaves) in the
// set.
func (vals *ValidatorSet) Hash() []byte {
bzs := make([][]byte, len(vals.Validators))
for i, val := range vals.Validators {
bzs[i] = val.Bytes()
}
return merkle.HashFromByteSlices(bzs)
}
// Iterate will run the given function over the set.
func (vals *ValidatorSet) Iterate(fn func(index int, val *Validator) bool) {
for i, val := range vals.Validators {
stop := fn(i, val.Copy())
if stop {
break
}
}
}
// Checks changes against duplicates, splits the changes in updates and
// removals, sorts them by address.
//
// Returns:
// updates, removals - the sorted lists of updates and removals
// err - non-nil if duplicate entries or entries with negative voting power are seen
//
// No changes are made to 'origChanges'.
func processChanges(origChanges []*Validator) (updates, removals []*Validator, err error) {
// Make a deep copy of the changes and sort by address.
changes := validatorListCopy(origChanges)
sort.Sort(ValidatorsByAddress(changes))
removals = make([]*Validator, 0, len(changes))
updates = make([]*Validator, 0, len(changes))
var prevAddr Address
// Scan changes by address and append valid validators to updates or removals lists.
for _, valUpdate := range changes {
if bytes.Equal(valUpdate.Address, prevAddr) {
err = fmt.Errorf("duplicate entry %v in %v", valUpdate, changes)
return nil, nil, err
}
switch {
case valUpdate.VotingPower < 0:
err = fmt.Errorf("voting power can't be negative: %d", valUpdate.VotingPower)
return nil, nil, err
case valUpdate.VotingPower > MaxTotalVotingPower:
err = fmt.Errorf("to prevent clipping/overflow, voting power can't be higher than %d, got %d",
MaxTotalVotingPower, valUpdate.VotingPower)
return nil, nil, err
case valUpdate.VotingPower == 0:
removals = append(removals, valUpdate)
default:
updates = append(updates, valUpdate)
}
prevAddr = valUpdate.Address
}
return updates, removals, err
}
// verifyUpdates verifies a list of updates against a validator set, making sure the allowed
// total voting power would not be exceeded if these updates would be applied to the set.
//
// Inputs:
// updates - a list of proper validator changes, i.e. they have been verified by processChanges for duplicates
// and invalid values.
// vals - the original validator set. Note that vals is NOT modified by this function.
// removedPower - the total voting power that will be removed after the updates are verified and applied.
//
// Returns:
// tvpAfterUpdatesBeforeRemovals - the new total voting power if these updates would be applied without the removals.
// Note that this will be < 2 * MaxTotalVotingPower in case high power validators are removed and
// validators are added/ updated with high power values.
//
// err - non-nil if the maximum allowed total voting power would be exceeded
func verifyUpdates(
updates []*Validator,
vals *ValidatorSet,
removedPower int64,
) (tvpAfterUpdatesBeforeRemovals int64, err error) {
delta := func(update *Validator, vals *ValidatorSet) int64 {
_, val := vals.GetByAddress(update.Address)
if val != nil {
return update.VotingPower - val.VotingPower
}
return update.VotingPower
}
updatesCopy := validatorListCopy(updates)
sort.Slice(updatesCopy, func(i, j int) bool {
return delta(updatesCopy[i], vals) < delta(updatesCopy[j], vals)
})
tvpAfterRemovals := vals.TotalVotingPower() - removedPower
for _, upd := range updatesCopy {
tvpAfterRemovals += delta(upd, vals)
if tvpAfterRemovals > MaxTotalVotingPower {
return 0, ErrTotalVotingPowerOverflow
}
}
return tvpAfterRemovals + removedPower, nil
}
func numNewValidators(updates []*Validator, vals *ValidatorSet) int {
numNewValidators := 0
for _, valUpdate := range updates {
if !vals.HasAddress(valUpdate.Address) {
numNewValidators++
}
}
return numNewValidators
}
// computeNewPriorities computes the proposer priority for the validators not present in the set based on
// 'updatedTotalVotingPower'.
// Leaves unchanged the priorities of validators that are changed.
//
// 'updates' parameter must be a list of unique validators to be added or updated.
//
// 'updatedTotalVotingPower' is the total voting power of a set where all updates would be applied but
// not the removals. It must be < 2*MaxTotalVotingPower and may be close to this limit if close to
// MaxTotalVotingPower will be removed. This is still safe from overflow since MaxTotalVotingPower is maxInt64/8.
//
// No changes are made to the validator set 'vals'.
func computeNewPriorities(updates []*Validator, vals *ValidatorSet, updatedTotalVotingPower int64) {
for _, valUpdate := range updates {
address := valUpdate.Address
_, val := vals.GetByAddress(address)
if val == nil {
// add val
// Set ProposerPriority to -C*totalVotingPower (with C ~= 1.125) to make sure validators can't
// un-bond and then re-bond to reset their (potentially previously negative) ProposerPriority to zero.
//
// Contract: updatedVotingPower < 2 * MaxTotalVotingPower to ensure ProposerPriority does
// not exceed the bounds of int64.
//
// Compute ProposerPriority = -1.125*totalVotingPower == -(updatedVotingPower + (updatedVotingPower >> 3)).
valUpdate.ProposerPriority = -(updatedTotalVotingPower + (updatedTotalVotingPower >> 3))
} else {
valUpdate.ProposerPriority = val.ProposerPriority
}
}
}
// Merges the vals' validator list with the updates list.
// When two elements with same address are seen, the one from updates is selected.
// Expects updates to be a list of updates sorted by address with no duplicates or errors,
// must have been validated with verifyUpdates() and priorities computed with computeNewPriorities().
func (vals *ValidatorSet) applyUpdates(updates []*Validator) {
existing := vals.Validators
sort.Sort(ValidatorsByAddress(existing))
merged := make([]*Validator, len(existing)+len(updates))
i := 0
for len(existing) > 0 && len(updates) > 0 {
if bytes.Compare(existing[0].Address, updates[0].Address) < 0 { // unchanged validator
merged[i] = existing[0]
existing = existing[1:]
} else {
// Apply add or update.
merged[i] = updates[0]
if bytes.Equal(existing[0].Address, updates[0].Address) {
// Validator is present in both, advance existing.
existing = existing[1:]
}
updates = updates[1:]
}
i++
}
// Add the elements which are left.
for j := 0; j < len(existing); j++ {
merged[i] = existing[j]
i++
}
// OR add updates which are left.
for j := 0; j < len(updates); j++ {
merged[i] = updates[j]
i++
}
vals.Validators = merged[:i]
}
// Checks that the validators to be removed are part of the validator set.
// No changes are made to the validator set 'vals'.
func verifyRemovals(deletes []*Validator, vals *ValidatorSet) (votingPower int64, err error) {
removedVotingPower := int64(0)
for _, valUpdate := range deletes {
address := valUpdate.Address
_, val := vals.GetByAddress(address)
if val == nil {
return removedVotingPower, fmt.Errorf("failed to find validator %X to remove", address)
}
removedVotingPower += val.VotingPower
}
if len(deletes) > len(vals.Validators) {
panic("more deletes than validators")
}
return removedVotingPower, nil
}
// Removes the validators specified in 'deletes' from validator set 'vals'.
// Should not fail as verification has been done before.
// Expects vals to be sorted by address (done by applyUpdates).
func (vals *ValidatorSet) applyRemovals(deletes []*Validator) {
existing := vals.Validators
merged := make([]*Validator, len(existing)-len(deletes))
i := 0
// Loop over deletes until we removed all of them.
for len(deletes) > 0 {
if bytes.Equal(existing[0].Address, deletes[0].Address) {
deletes = deletes[1:]
} else { // Leave it in the resulting slice.
merged[i] = existing[0]
i++
}
existing = existing[1:]
}
// Add the elements which are left.
for j := 0; j < len(existing); j++ {
merged[i] = existing[j]
i++
}
vals.Validators = merged[:i]
}
// Main function used by UpdateWithChangeSet() and NewValidatorSet().
// If 'allowDeletes' is false then delete operations (identified by validators with voting power 0)
// are not allowed and will trigger an error if present in 'changes'.
// The 'allowDeletes' flag is set to false by NewValidatorSet() and to true by UpdateWithChangeSet().
func (vals *ValidatorSet) updateWithChangeSet(changes []*Validator, allowDeletes bool) error {
if len(changes) == 0 {
return nil
}
// Check for duplicates within changes, split in 'updates' and 'deletes' lists (sorted).
updates, deletes, err := processChanges(changes)
if err != nil {
return err
}
if !allowDeletes && len(deletes) != 0 {
return fmt.Errorf("cannot process validators with voting power 0: %v", deletes)
}
// Check that the resulting set will not be empty.
if numNewValidators(updates, vals) == 0 && len(vals.Validators) == len(deletes) {
return errors.New("applying the validator changes would result in empty set")
}
// Verify that applying the 'deletes' against 'vals' will not result in error.
// Get the voting power that is going to be removed.
removedVotingPower, err := verifyRemovals(deletes, vals)
if err != nil {
return err
}
// Verify that applying the 'updates' against 'vals' will not result in error.
// Get the updated total voting power before removal. Note that this is < 2 * MaxTotalVotingPower
tvpAfterUpdatesBeforeRemovals, err := verifyUpdates(updates, vals, removedVotingPower)
if err != nil {
return err
}
// Compute the priorities for updates.
computeNewPriorities(updates, vals, tvpAfterUpdatesBeforeRemovals)
// Apply updates and removals.
vals.applyUpdates(updates)
vals.applyRemovals(deletes)
vals.updateTotalVotingPower() // will panic if total voting power > MaxTotalVotingPower
// Scale and center.
vals.RescalePriorities(PriorityWindowSizeFactor * vals.TotalVotingPower())
vals.shiftByAvgProposerPriority()
sort.Sort(ValidatorsByVotingPower(vals.Validators))
return nil
}
// UpdateWithChangeSet attempts to update the validator set with 'changes'.
// It performs the following steps:
// - validates the changes making sure there are no duplicates and splits them in updates and deletes
// - verifies that applying the changes will not result in errors
// - computes the total voting power BEFORE removals to ensure that in the next steps the priorities
// across old and newly added validators are fair
// - computes the priorities of new validators against the final set
// - applies the updates against the validator set
// - applies the removals against the validator set
// - performs scaling and centering of priority values
// If an error is detected during verification steps, it is returned and the validator set
// is not changed.
func (vals *ValidatorSet) UpdateWithChangeSet(changes []*Validator) error {
return vals.updateWithChangeSet(changes, true)
}
// VerifyCommit verifies +2/3 of the set had signed the given commit and all
// other signatures are valid
func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID,
height int64, commit *Commit) error {
return VerifyCommit(chainID, vals, blockID, height, commit)
}
// LIGHT CLIENT VERIFICATION METHODS
// VerifyCommitLight verifies +2/3 of the set had signed the given commit.
func (vals *ValidatorSet) VerifyCommitLight(chainID string, blockID BlockID,
height int64, commit *Commit) error {
return VerifyCommitLight(chainID, vals, blockID, height, commit)
}
// VerifyCommitLightTrusting verifies that trustLevel of the validator set signed
// this commit.
func (vals *ValidatorSet) VerifyCommitLightTrusting(chainID string, commit *Commit, trustLevel tmmath.Fraction) error {
return VerifyCommitLightTrusting(chainID, vals, commit, trustLevel)
}
// findPreviousProposer reverses the compare proposer priority function to find the validator
// with the lowest proposer priority which would have been the previous proposer.
//
// Is used when recreating a validator set from an existing array of validators.
func (vals *ValidatorSet) findPreviousProposer() *Validator {
var previousProposer *Validator
for _, val := range vals.Validators {
if previousProposer == nil {
previousProposer = val
continue
}
if previousProposer == previousProposer.CompareProposerPriority(val) {
previousProposer = val
}
}
return previousProposer
}
//-----------------
// IsErrNotEnoughVotingPowerSigned returns true if err is
// ErrNotEnoughVotingPowerSigned.
func IsErrNotEnoughVotingPowerSigned(err error) bool {
return errors.As(err, &ErrNotEnoughVotingPowerSigned{})
}
// ErrNotEnoughVotingPowerSigned is returned when not enough validators signed
// a commit.
type ErrNotEnoughVotingPowerSigned struct {
Got int64
Needed int64
}
func (e ErrNotEnoughVotingPowerSigned) Error() string {
return fmt.Sprintf("invalid commit -- insufficient voting power: got %d, needed more than %d", e.Got, e.Needed)
}
//----------------
// String returns a string representation of ValidatorSet.
//
// See StringIndented.
func (vals *ValidatorSet) String() string {
return vals.StringIndented("")
}
// StringIndented returns an intended String.
//
// See Validator#String.
func (vals *ValidatorSet) StringIndented(indent string) string {
if vals == nil {
return "nil-ValidatorSet"
}
var valStrings []string
vals.Iterate(func(index int, val *Validator) bool {
valStrings = append(valStrings, val.String())
return false
})
return fmt.Sprintf(`ValidatorSet{
%s Proposer: %v
%s Validators:
%s %v
%s}`,
indent, vals.GetProposer().String(),
indent,
indent, strings.Join(valStrings, "\n"+indent+" "),
indent)
}
//-------------------------------------
// ValidatorsByVotingPower implements sort.Interface for []*Validator based on
// the VotingPower and Address fields.
type ValidatorsByVotingPower []*Validator
func (valz ValidatorsByVotingPower) Len() int { return len(valz) }
func (valz ValidatorsByVotingPower) Less(i, j int) bool {
if valz[i].VotingPower == valz[j].VotingPower {
return bytes.Compare(valz[i].Address, valz[j].Address) == -1
}
return valz[i].VotingPower > valz[j].VotingPower
}
func (valz ValidatorsByVotingPower) Swap(i, j int) {
valz[i], valz[j] = valz[j], valz[i]
}
// ValidatorsByAddress implements sort.Interface for []*Validator based on
// the Address field.
type ValidatorsByAddress []*Validator
func (valz ValidatorsByAddress) Len() int { return len(valz) }
func (valz ValidatorsByAddress) Less(i, j int) bool {
return bytes.Compare(valz[i].Address, valz[j].Address) == -1
}
func (valz ValidatorsByAddress) Swap(i, j int) {
valz[i], valz[j] = valz[j], valz[i]
}
// ToProto converts ValidatorSet to protobuf
func (vals *ValidatorSet) ToProto() (*tmproto.ValidatorSet, error) {
if vals.IsNilOrEmpty() {
return &tmproto.ValidatorSet{}, nil // validator set should never be nil
}
vp := new(tmproto.ValidatorSet)
valsProto := make([]*tmproto.Validator, len(vals.Validators))
for i := 0; i < len(vals.Validators); i++ {
valp, err := vals.Validators[i].ToProto()
if err != nil {
return nil, err
}
valsProto[i] = valp
}
vp.Validators = valsProto
valProposer, err := vals.Proposer.ToProto()
if err != nil {
return nil, fmt.Errorf("toProto: validatorSet proposer error: %w", err)
}
vp.Proposer = valProposer
// NOTE: Sometimes we use the bytes of the proto form as a hash. This means that we need to
// be consistent with cached data
vp.TotalVotingPower = 0
return vp, nil
}
// ValidatorSetFromProto sets a protobuf ValidatorSet to the given pointer.
// It returns an error if any of the validators from the set or the proposer
// is invalid
func ValidatorSetFromProto(vp *tmproto.ValidatorSet) (*ValidatorSet, error) {
if vp == nil {
return nil, errors.New("nil validator set") // validator set should never be nil, bigger issues are at play if empty
}
vals := new(ValidatorSet)
valsProto := make([]*Validator, len(vp.Validators))
for i := 0; i < len(vp.Validators); i++ {
v, err := ValidatorFromProto(vp.Validators[i])
if err != nil {
return nil, err
}
valsProto[i] = v
}
vals.Validators = valsProto
p, err := ValidatorFromProto(vp.GetProposer())
if err != nil {
return nil, fmt.Errorf("fromProto: validatorSet proposer error: %w", err)
}
vals.Proposer = p
// NOTE: We can't trust the total voting power given to us by other peers. If someone were to
// inject a non-zeo value that wasn't the correct voting power we could assume a wrong total
// power hence we need to recompute it.
// FIXME: We should look to remove TotalVotingPower from proto or add it in the validators hash
// so we don't have to do this
vals.TotalVotingPower()
return vals, vals.ValidateBasic()
}
// ValidatorSetFromExistingValidators takes an existing array of validators and
// rebuilds the exact same validator set that corresponds to it without
// changing the proposer priority or power if any of the validators fail
// validate basic then an empty set is returned.
func ValidatorSetFromExistingValidators(valz []*Validator) (*ValidatorSet, error) {
if len(valz) == 0 {
return nil, errors.New("validator set is empty")
}
for _, val := range valz {
err := val.ValidateBasic()
if err != nil {
return nil, fmt.Errorf("can't create validator set: %w", err)
}
}
vals := &ValidatorSet{
Validators: valz,
}
vals.Proposer = vals.findPreviousProposer()
vals.updateTotalVotingPower()
sort.Sort(ValidatorsByVotingPower(vals.Validators))
return vals, nil
}
//----------------------------------------
// safe addition/subtraction/multiplication
func safeAdd(a, b int64) (int64, bool) {
if b > 0 && a > math.MaxInt64-b {
return -1, true
} else if b < 0 && a < math.MinInt64-b {
return -1, true
}
return a + b, false
}
func safeSub(a, b int64) (int64, bool) {
if b > 0 && a < math.MinInt64+b {
return -1, true
} else if b < 0 && a > math.MaxInt64+b {
return -1, true
}
return a - b, false
}
func safeAddClip(a, b int64) int64 {
c, overflow := safeAdd(a, b)
if overflow {
if b < 0 {
return math.MinInt64
}
return math.MaxInt64
}
return c
}
func safeSubClip(a, b int64) int64 {
c, overflow := safeSub(a, b)
if overflow {
if b > 0 {
return math.MinInt64
}
return math.MaxInt64
}
return c
}
func safeMul(a, b int64) (int64, bool) {
if a == 0 || b == 0 {
return 0, false
}
absOfB := b
if b < 0 {
absOfB = -b
}
absOfA := a
if a < 0 {
absOfA = -a
}
if absOfA > math.MaxInt64/absOfB {
return 0, true
}
return a * b, false
}
File diff suppressed because it is too large Load Diff
+119
View File
@@ -0,0 +1,119 @@
package block
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
)
func TestValidatorProtoBuf(t *testing.T) {
val, _ := randValidator(true, 100)
testCases := []struct {
msg string
v1 *Validator
expPass1 bool
expPass2 bool
}{
{"success validator", val, true, true},
{"failure empty", &Validator{}, false, false},
{"failure nil", nil, false, false},
}
for _, tc := range testCases {
protoVal, err := tc.v1.ToProto()
if tc.expPass1 {
require.NoError(t, err, tc.msg)
} else {
require.Error(t, err, tc.msg)
}
val, err := ValidatorFromProto(protoVal)
if tc.expPass2 {
require.NoError(t, err, tc.msg)
require.Equal(t, tc.v1, val, tc.msg)
} else {
require.Error(t, err, tc.msg)
}
}
}
func TestValidatorValidateBasic(t *testing.T) {
priv := NewMockPV()
pubKey, _ := priv.GetPubKey(context.Background())
testCases := []struct {
val *Validator
err bool
msg string
}{
{
val: NewValidator(pubKey, 1),
err: false,
msg: "",
},
{
val: nil,
err: true,
msg: "nil validator",
},
{
val: &Validator{
PubKey: nil,
},
err: true,
msg: "validator does not have a public key",
},
{
val: NewValidator(pubKey, -1),
err: true,
msg: "validator has negative voting power",
},
{
val: &Validator{
PubKey: pubKey,
Address: nil,
},
err: true,
msg: "validator address is the wrong size: ",
},
{
val: &Validator{
PubKey: pubKey,
Address: []byte{'a'},
},
err: true,
msg: "validator address is the wrong size: 61",
},
}
for _, tc := range testCases {
err := tc.val.ValidateBasic()
if tc.err {
if assert.Error(t, err) {
assert.Equal(t, tc.msg, err.Error())
}
} else {
assert.NoError(t, err)
}
}
}
// Testing util functions
// deterministicValidator returns a deterministic validator, useful for testing.
// UNSTABLE
func deterministicValidator(key crypto.PrivKey) (*Validator, PrivValidator) {
privVal := NewMockPV()
privVal.PrivKey = key
var votePower int64 = 50
pubKey, err := privVal.GetPubKey(context.TODO())
if err != nil {
panic(fmt.Errorf("could not retrieve pubkey %w", err))
}
val := NewValidator(pubKey, votePower)
return val, privVal
}
+246
View File
@@ -0,0 +1,246 @@
package block
import (
"bytes"
"errors"
"fmt"
"time"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/internal/libs/protoio"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
const (
nilVoteStr string = "nil-Vote"
)
var (
ErrVoteUnexpectedStep = errors.New("unexpected step")
ErrVoteInvalidValidatorIndex = errors.New("invalid validator index")
ErrVoteInvalidValidatorAddress = errors.New("invalid validator address")
ErrVoteInvalidSignature = errors.New("invalid signature")
ErrVoteInvalidBlockHash = errors.New("invalid block hash")
ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature")
ErrVoteNil = errors.New("nil vote")
)
type ErrVoteConflictingVotes struct {
VoteA *Vote
VoteB *Vote
}
func (err *ErrVoteConflictingVotes) Error() string {
return fmt.Sprintf("conflicting votes from validator %X", err.VoteA.ValidatorAddress)
}
func NewConflictingVoteError(vote1, vote2 *Vote) *ErrVoteConflictingVotes {
return &ErrVoteConflictingVotes{
VoteA: vote1,
VoteB: vote2,
}
}
// Address is hex bytes.
type Address = crypto.Address
// Vote represents a prevote, precommit, or commit vote from validators for
// consensus.
type Vote struct {
Type tmproto.SignedMsgType `json:"type"`
Height int64 `json:"height"`
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"`
}
// CommitSig converts the Vote to a CommitSig.
func (vote *Vote) CommitSig() CommitSig {
if vote == nil {
return NewCommitSigAbsent()
}
var blockIDFlag BlockIDFlag
switch {
case vote.BlockID.IsComplete():
blockIDFlag = BlockIDFlagCommit
case vote.BlockID.IsZero():
blockIDFlag = BlockIDFlagNil
default:
panic(fmt.Sprintf("Invalid vote %v - expected BlockID to be either empty or complete", vote))
}
return CommitSig{
BlockIDFlag: blockIDFlag,
ValidatorAddress: vote.ValidatorAddress,
Timestamp: vote.Timestamp,
Signature: vote.Signature,
}
}
// VoteSignBytes returns the proto-encoding of the canonicalized Vote, for
// signing. Panics is the marshaling fails.
//
// The encoded Protobuf message is varint length-prefixed (using MarshalDelimited)
// for backwards-compatibility with the Amino encoding, due to e.g. hardware
// devices that rely on this encoding.
//
// See CanonicalizeVote
func VoteSignBytes(chainID string, vote *tmproto.Vote) []byte {
pb := CanonicalizeVote(chainID, vote)
bz, err := protoio.MarshalDelimited(&pb)
if err != nil {
panic(err)
}
return bz
}
func (vote *Vote) Copy() *Vote {
voteCopy := *vote
return &voteCopy
}
// String returns a string representation of Vote.
//
// 1. validator index
// 2. first 6 bytes of validator address
// 3. height
// 4. round,
// 5. type byte
// 6. type string
// 7. first 6 bytes of block hash
// 8. first 6 bytes of signature
// 9. timestamp
func (vote *Vote) String() string {
if vote == nil {
return nilVoteStr
}
var typeString string
switch vote.Type {
case tmproto.PrevoteType:
typeString = "Prevote"
case tmproto.PrecommitType:
typeString = "Precommit"
default:
panic("Unknown vote type")
}
return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %X @ %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 {
if !bytes.Equal(pubKey.Address(), vote.ValidatorAddress) {
return ErrVoteInvalidValidatorAddress
}
v := vote.ToProto()
if !pubKey.VerifySignature(VoteSignBytes(chainID, v), vote.Signature) {
return ErrVoteInvalidSignature
}
return nil
}
// ValidateBasic performs basic validation.
func (vote *Vote) ValidateBasic() error {
if !IsVoteTypeValid(vote.Type) {
return errors.New("invalid Type")
}
if vote.Height < 0 {
return errors.New("negative Height")
}
if vote.Round < 0 {
return errors.New("negative Round")
}
// NOTE: Timestamp validation is subtle and handled elsewhere.
if err := vote.BlockID.ValidateBasic(); err != nil {
return fmt.Errorf("wrong BlockID: %v", err)
}
// 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() {
return fmt.Errorf("blockID must be either empty or complete, got: %v", vote.BlockID)
}
if len(vote.ValidatorAddress) != crypto.AddressSize {
return fmt.Errorf("expected ValidatorAddress size to be %d bytes, got %d bytes",
crypto.AddressSize,
len(vote.ValidatorAddress),
)
}
if vote.ValidatorIndex < 0 {
return errors.New("negative ValidatorIndex")
}
if len(vote.Signature) == 0 {
return errors.New("signature is missing")
}
if len(vote.Signature) > MaxSignatureSize {
return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize)
}
return nil
}
// ToProto converts the handwritten type to proto generated type
// return type, nil if everything converts safely, otherwise nil, error
func (vote *Vote) ToProto() *tmproto.Vote {
if vote == nil {
return nil
}
return &tmproto.Vote{
Type: vote.Type,
Height: vote.Height,
Round: vote.Round,
BlockID: vote.BlockID.ToProto(),
Timestamp: vote.Timestamp,
ValidatorAddress: vote.ValidatorAddress,
ValidatorIndex: vote.ValidatorIndex,
Signature: vote.Signature,
}
}
// FromProto converts a proto generetad type to a handwritten type
// return type, nil if everything converts safely, otherwise nil, error
func VoteFromProto(pv *tmproto.Vote) (*Vote, error) {
if pv == nil {
return nil, errors.New("nil vote")
}
blockID, err := BlockIDFromProto(&pv.BlockID)
if err != nil {
return nil, err
}
vote := new(Vote)
vote.Type = pv.Type
vote.Height = pv.Height
vote.Round = pv.Round
vote.BlockID = *blockID
vote.Timestamp = pv.Timestamp
vote.ValidatorAddress = pv.ValidatorAddress
vote.ValidatorIndex = pv.ValidatorIndex
vote.Signature = pv.Signature
return vote, vote.ValidateBasic()
}
+670
View File
@@ -0,0 +1,670 @@
package block
import (
"bytes"
"fmt"
"strings"
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
"github.com/tendermint/tendermint/libs/bits"
tmjson "github.com/tendermint/tendermint/libs/json"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
const (
// MaxVotesCount is the maximum number of votes in a set. Used in ValidateBasic funcs for
// protection against DOS attacks. Note this implies a corresponding equal limit to
// the number of validators.
MaxVotesCount = 10000
)
// UNSTABLE
// XXX: duplicate of p2p.ID to avoid dependence between packages.
// Perhaps we can have a minimal types package containing this (and other things?)
// that both `types` and `p2p` import ?
type P2PID string
/*
VoteSet helps collect signatures from validators at each height+round for a
predefined vote type.
We need VoteSet to be able to keep track of conflicting votes when validators
double-sign. Yet, we can't keep track of *all* the votes seen, as that could
be a DoS attack vector.
There are two storage areas for votes.
1. voteSet.votes
2. voteSet.votesByBlock
`.votes` is the "canonical" list of votes. It always has at least one vote,
if a vote from a validator had been seen at all. Usually it keeps track of
the first vote seen, but when a 2/3 majority is found, votes for that get
priority and are copied over from `.votesByBlock`.
`.votesByBlock` keeps track of a list of votes for a particular block. There
are two ways a &blockVotes{} gets created in `.votesByBlock`.
1. the first vote seen by a validator was for the particular block.
2. a peer claims to have seen 2/3 majority for the particular block.
Since the first vote from a validator will always get added in `.votesByBlock`
, all votes in `.votes` will have a corresponding entry in `.votesByBlock`.
When a &blockVotes{} in `.votesByBlock` reaches a 2/3 majority quorum, its
votes are copied into `.votes`.
All this is memory bounded because conflicting votes only get added if a peer
told us to track that block, each peer only gets to tell us 1 such block, and,
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
mtx tmsync.Mutex
votesBitArray *bits.BitArray
votes []*Vote // Primary votes to share
sum int64 // Sum of voting power for seen votes, discounting conflicts
maj23 *BlockID // First 2/3 majority seen
votesByBlock map[string]*blockVotes // string(blockHash|blockParts) -> blockVotes
peerMaj23s map[P2PID]BlockID // Maj23 for each peer
}
// Constructs a new VoteSet struct used to accumulate votes for given height/round.
func NewVoteSet(chainID string, height int64, round int32,
signedMsgType tmproto.SignedMsgType, valSet *ValidatorSet) *VoteSet {
if height == 0 {
panic("Cannot make VoteSet for height == 0, doesn't make sense.")
}
return &VoteSet{
chainID: chainID,
height: height,
round: round,
signedMsgType: signedMsgType,
valSet: valSet,
votesBitArray: bits.NewBitArray(valSet.Size()),
votes: make([]*Vote, valSet.Size()),
sum: 0,
maj23: nil,
votesByBlock: make(map[string]*blockVotes, valSet.Size()),
peerMaj23s: make(map[P2PID]BlockID),
}
}
func (voteSet *VoteSet) ChainID() string {
return voteSet.chainID
}
// Implements VoteSetReader.
func (voteSet *VoteSet) GetHeight() int64 {
if voteSet == nil {
return 0
}
return voteSet.height
}
// Implements VoteSetReader.
func (voteSet *VoteSet) GetRound() int32 {
if voteSet == nil {
return -1
}
return voteSet.round
}
// Implements VoteSetReader.
func (voteSet *VoteSet) Type() byte {
if voteSet == nil {
return 0x00
}
return byte(voteSet.signedMsgType)
}
// Implements VoteSetReader.
func (voteSet *VoteSet) Size() int {
if voteSet == nil {
return 0
}
return voteSet.valSet.Size()
}
// Returns added=true if vote is valid and new.
// Otherwise returns err=ErrVote[
// UnexpectedStep | InvalidIndex | InvalidAddress |
// InvalidSignature | InvalidBlockHash | ConflictingVotes ]
// Duplicate votes return added=false, err=nil.
// Conflicting votes return added=*, err=ErrVoteConflictingVotes.
// NOTE: vote should not be mutated after adding.
// NOTE: VoteSet must not be nil
// NOTE: Vote must not be nil
func (voteSet *VoteSet) AddVote(vote *Vote) (added bool, err error) {
if voteSet == nil {
panic("AddVote() on nil VoteSet")
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.addVote(vote)
}
// NOTE: Validates as much as possible before attempting to verify the signature.
func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) {
if vote == nil {
return false, ErrVoteNil
}
valIndex := vote.ValidatorIndex
valAddr := vote.ValidatorAddress
blockKey := vote.BlockID.Key()
// Ensure that validator index was set
if valIndex < 0 {
return false, fmt.Errorf("index < 0: %w", ErrVoteInvalidValidatorIndex)
} else if len(valAddr) == 0 {
return false, fmt.Errorf("empty address: %w", ErrVoteInvalidValidatorAddress)
}
// Make sure the step matches.
if (vote.Height != voteSet.height) ||
(vote.Round != voteSet.round) ||
(vote.Type != voteSet.signedMsgType) {
return false, fmt.Errorf("expected %d/%d/%d, but got %d/%d/%d: %w",
voteSet.height, voteSet.round, voteSet.signedMsgType,
vote.Height, vote.Round, vote.Type, ErrVoteUnexpectedStep)
}
// Ensure that signer is a validator.
lookupAddr, val := voteSet.valSet.GetByIndex(valIndex)
if val == nil {
return false, fmt.Errorf(
"cannot find validator %d in valSet of size %d: %w",
valIndex, voteSet.valSet.Size(), ErrVoteInvalidValidatorIndex)
}
// Ensure that the signer has the right address.
if !bytes.Equal(valAddr, lookupAddr) {
return false, fmt.Errorf(
"vote.ValidatorAddress (%X) does not match address (%X) for vote.ValidatorIndex (%d)\n"+
"Ensure the genesis file is correct across all validators: %w",
valAddr, lookupAddr, valIndex, ErrVoteInvalidValidatorAddress)
}
// If we already know of this vote, return false.
if existing, ok := voteSet.getVote(valIndex, blockKey); ok {
if bytes.Equal(existing.Signature, vote.Signature) {
return false, nil // duplicate
}
return false, fmt.Errorf("existing vote: %v; new vote: %v: %w", existing, vote, ErrVoteNonDeterministicSignature)
}
// Check signature.
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)
}
// Add vote and get conflicting vote if any.
added, conflicting := voteSet.addVerifiedVote(vote, blockKey, val.VotingPower)
if conflicting != nil {
return added, NewConflictingVoteError(conflicting, vote)
}
if !added {
panic("Expected to add non-conflicting vote")
}
return added, nil
}
// Returns (vote, true) if vote exists for valIndex and blockKey.
func (voteSet *VoteSet) getVote(valIndex int32, blockKey string) (vote *Vote, ok bool) {
if existing := voteSet.votes[valIndex]; existing != nil && existing.BlockID.Key() == blockKey {
return existing, true
}
if existing := voteSet.votesByBlock[blockKey].getByIndex(valIndex); existing != nil {
return existing, true
}
return nil, false
}
// Assumes signature is valid.
// If conflicting vote exists, returns it.
func (voteSet *VoteSet) addVerifiedVote(
vote *Vote,
blockKey string,
votingPower int64,
) (added bool, conflicting *Vote) {
valIndex := vote.ValidatorIndex
// Already exists in voteSet.votes?
if existing := voteSet.votes[valIndex]; existing != nil {
if existing.BlockID.Equals(vote.BlockID) {
panic("addVerifiedVote does not expect duplicate votes")
} else {
conflicting = existing
}
// Replace vote if blockKey matches voteSet.maj23.
if voteSet.maj23 != nil && voteSet.maj23.Key() == blockKey {
voteSet.votes[valIndex] = vote
voteSet.votesBitArray.SetIndex(int(valIndex), true)
}
// Otherwise don't add it to voteSet.votes
} else {
// Add to voteSet.votes and incr .sum
voteSet.votes[valIndex] = vote
voteSet.votesBitArray.SetIndex(int(valIndex), true)
voteSet.sum += votingPower
}
votesByBlock, ok := voteSet.votesByBlock[blockKey]
if ok {
if conflicting != nil && !votesByBlock.peerMaj23 {
// There's a conflict and no peer claims that this block is special.
return false, conflicting
}
// We'll add the vote in a bit.
} else {
// .votesByBlock doesn't exist...
if conflicting != nil {
// ... and there's a conflicting vote.
// We're not even tracking this blockKey, so just forget it.
return false, conflicting
}
// ... and there's no conflicting vote.
// Start tracking this blockKey
votesByBlock = newBlockVotes(false, voteSet.valSet.Size())
voteSet.votesByBlock[blockKey] = votesByBlock
// We'll add the vote in a bit.
}
// Before adding to votesByBlock, see if we'll exceed quorum
origSum := votesByBlock.sum
quorum := voteSet.valSet.TotalVotingPower()*2/3 + 1
// Add vote to votesByBlock
votesByBlock.addVerifiedVote(vote, votingPower)
// If we just crossed the quorum threshold and have 2/3 majority...
if origSum < quorum && quorum <= votesByBlock.sum {
// Only consider the first quorum reached
if voteSet.maj23 == nil {
maj23BlockID := vote.BlockID
voteSet.maj23 = &maj23BlockID
// And also copy votes over to voteSet.votes
for i, vote := range votesByBlock.votes {
if vote != nil {
voteSet.votes[i] = vote
}
}
}
}
return true, conflicting
}
// If a peer claims that it has 2/3 majority for given blockKey, call this.
// NOTE: if there are too many peers, or too much peer churn,
// this can cause memory issues.
// TODO: implement ability to remove peers too
// NOTE: VoteSet must not be nil
func (voteSet *VoteSet) SetPeerMaj23(peerID P2PID, blockID BlockID) error {
if voteSet == nil {
panic("SetPeerMaj23() on nil VoteSet")
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
blockKey := blockID.Key()
// Make sure peer hasn't already told us something.
if existing, ok := voteSet.peerMaj23s[peerID]; ok {
if existing.Equals(blockID) {
return nil // Nothing to do
}
return fmt.Errorf("setPeerMaj23: Received conflicting blockID from peer %v. Got %v, expected %v",
peerID, blockID, existing)
}
voteSet.peerMaj23s[peerID] = blockID
// Create .votesByBlock entry if needed.
votesByBlock, ok := voteSet.votesByBlock[blockKey]
if ok {
if votesByBlock.peerMaj23 {
return nil // Nothing to do
}
votesByBlock.peerMaj23 = true
// No need to copy votes, already there.
} else {
votesByBlock = newBlockVotes(true, voteSet.valSet.Size())
voteSet.votesByBlock[blockKey] = votesByBlock
// No need to copy votes, no votes to copy over.
}
return nil
}
// Implements VoteSetReader.
func (voteSet *VoteSet) BitArray() *bits.BitArray {
if voteSet == nil {
return nil
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.votesBitArray.Copy()
}
func (voteSet *VoteSet) BitArrayByBlockID(blockID BlockID) *bits.BitArray {
if voteSet == nil {
return nil
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
votesByBlock, ok := voteSet.votesByBlock[blockID.Key()]
if ok {
return votesByBlock.bitArray.Copy()
}
return nil
}
// NOTE: if validator has conflicting votes, returns "canonical" vote
// Implements VoteSetReader.
func (voteSet *VoteSet) GetByIndex(valIndex int32) *Vote {
if voteSet == nil {
return nil
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.votes[valIndex]
}
func (voteSet *VoteSet) GetByAddress(address []byte) *Vote {
if voteSet == nil {
return nil
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
valIndex, val := voteSet.valSet.GetByAddress(address)
if val == nil {
panic("GetByAddress(address) returned nil")
}
return voteSet.votes[valIndex]
}
func (voteSet *VoteSet) HasTwoThirdsMajority() bool {
if voteSet == nil {
return false
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.maj23 != nil
}
// Implements VoteSetReader.
func (voteSet *VoteSet) IsCommit() bool {
if voteSet == nil {
return false
}
if voteSet.signedMsgType != tmproto.PrecommitType {
return false
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.maj23 != nil
}
func (voteSet *VoteSet) HasTwoThirdsAny() bool {
if voteSet == nil {
return false
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.sum > voteSet.valSet.TotalVotingPower()*2/3
}
func (voteSet *VoteSet) HasAll() bool {
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.sum == voteSet.valSet.TotalVotingPower()
}
// If there was a +2/3 majority for blockID, return blockID and true.
// Else, return the empty BlockID{} and false.
func (voteSet *VoteSet) TwoThirdsMajority() (blockID BlockID, ok bool) {
if voteSet == nil {
return BlockID{}, false
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
if voteSet.maj23 != nil {
return *voteSet.maj23, true
}
return BlockID{}, false
}
//--------------------------------------------------------------------------------
// Strings and JSON
const nilVoteSetString = "nil-VoteSet"
// String returns a string representation of VoteSet.
//
// See StringIndented.
func (voteSet *VoteSet) String() string {
if voteSet == nil {
return nilVoteSetString
}
return voteSet.StringIndented("")
}
// StringIndented returns an indented String.
//
// Height Round Type
// Votes
// Votes bit array
// 2/3+ majority
//
// See Vote#String.
func (voteSet *VoteSet) StringIndented(indent string) string {
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
voteStrings := make([]string, len(voteSet.votes))
for i, vote := range voteSet.votes {
if vote == nil {
voteStrings[i] = nilVoteStr
} else {
voteStrings[i] = vote.String()
}
}
return fmt.Sprintf(`VoteSet{
%s H:%v R:%v T:%v
%s %v
%s %v
%s %v
%s}`,
indent, voteSet.height, voteSet.round, voteSet.signedMsgType,
indent, strings.Join(voteStrings, "\n"+indent+" "),
indent, voteSet.votesBitArray,
indent, voteSet.peerMaj23s,
indent)
}
// Marshal the VoteSet to JSON. Same as String(), just in JSON,
// and without the height/round/signedMsgType (since its already included in the votes).
func (voteSet *VoteSet) MarshalJSON() ([]byte, error) {
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return tmjson.Marshal(VoteSetJSON{
voteSet.voteStrings(),
voteSet.bitArrayString(),
voteSet.peerMaj23s,
})
}
// More human readable JSON of the vote set
// NOTE: insufficient for unmarshaling from (compressed votes)
// TODO: make the peerMaj23s nicer to read (eg just the block hash)
type VoteSetJSON struct {
Votes []string `json:"votes"`
VotesBitArray string `json:"votes_bit_array"`
PeerMaj23s map[P2PID]BlockID `json:"peer_maj_23s"`
}
// Return the bit-array of votes including
// the fraction of power that has voted like:
// "BA{29:xx__x__x_x___x__x_______xxx__} 856/1304 = 0.66"
func (voteSet *VoteSet) BitArrayString() string {
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.bitArrayString()
}
func (voteSet *VoteSet) bitArrayString() string {
bAString := voteSet.votesBitArray.String()
voted, total, fracVoted := voteSet.sumTotalFrac()
return fmt.Sprintf("%s %d/%d = %.2f", bAString, voted, total, fracVoted)
}
// Returns a list of votes compressed to more readable strings.
func (voteSet *VoteSet) VoteStrings() []string {
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
return voteSet.voteStrings()
}
func (voteSet *VoteSet) voteStrings() []string {
voteStrings := make([]string, len(voteSet.votes))
for i, vote := range voteSet.votes {
if vote == nil {
voteStrings[i] = nilVoteStr
} else {
voteStrings[i] = vote.String()
}
}
return voteStrings
}
// StringShort returns a short representation of VoteSet.
//
// 1. height
// 2. round
// 3. signed msg type
// 4. first 2/3+ majority
// 5. fraction of voted power
// 6. votes bit array
// 7. 2/3+ majority for each peer
func (voteSet *VoteSet) StringShort() string {
if voteSet == nil {
return nilVoteSetString
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
_, _, frac := voteSet.sumTotalFrac()
return fmt.Sprintf(`VoteSet{H:%v R:%v T:%v +2/3:%v(%v) %v %v}`,
voteSet.height, voteSet.round, voteSet.signedMsgType, voteSet.maj23, frac, voteSet.votesBitArray, voteSet.peerMaj23s)
}
// LogString produces a logging suitable string representation of the
// vote set.
func (voteSet *VoteSet) LogString() string {
if voteSet == nil {
return nilVoteSetString
}
voteSet.mtx.Lock()
defer voteSet.mtx.Unlock()
voted, total, frac := voteSet.sumTotalFrac()
return fmt.Sprintf("Votes:%d/%d(%.3f)", voted, total, frac)
}
// return the power voted, the total, and the fraction
func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) {
voted, total := voteSet.sum, voteSet.valSet.TotalVotingPower()
fracVoted := float64(voted) / float64(total)
return voted, total, fracVoted
}
//--------------------------------------------------------------------------------
// Commit
// MakeCommit 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 {
if voteSet.signedMsgType != tmproto.PrecommitType {
panic("Cannot MakeCommit() 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")
}
// For every validator, get the precommit
commitSigs := make([]CommitSig, len(voteSet.votes))
for i, v := range voteSet.votes {
commitSig := v.CommitSig()
// if block ID exists but doesn't match, exclude sig
if commitSig.ForBlock() && !v.BlockID.Equals(*voteSet.maj23) {
commitSig = NewCommitSigAbsent()
}
commitSigs[i] = commitSig
}
return NewCommit(voteSet.GetHeight(), voteSet.GetRound(), *voteSet.maj23, commitSigs)
}
//--------------------------------------------------------------------------------
/*
Votes for a particular block
There are two ways a *blockVotes gets created for a blockKey.
1. first (non-conflicting) vote of a validator w/ blockKey (peerMaj23=false)
2. A peer claims to have a 2/3 majority w/ blockKey (peerMaj23=true)
*/
type blockVotes struct {
peerMaj23 bool // peer claims to have maj23
bitArray *bits.BitArray // valIndex -> hasVote?
votes []*Vote // valIndex -> *Vote
sum int64 // vote sum
}
func newBlockVotes(peerMaj23 bool, numValidators int) *blockVotes {
return &blockVotes{
peerMaj23: peerMaj23,
bitArray: bits.NewBitArray(numValidators),
votes: make([]*Vote, numValidators),
sum: 0,
}
}
func (vs *blockVotes) addVerifiedVote(vote *Vote, votingPower int64) {
valIndex := vote.ValidatorIndex
if existing := vs.votes[valIndex]; existing == nil {
vs.bitArray.SetIndex(int(valIndex), true)
vs.votes[valIndex] = vote
vs.sum += votingPower
}
}
func (vs *blockVotes) getByIndex(index int32) *Vote {
if vs == nil {
return nil
}
return vs.votes[index]
}
//--------------------------------------------------------------------------------
// Common interface between *consensus.VoteSet and types.Commit
type VoteSetReader interface {
GetHeight() int64
GetRound() int32
Type() byte
Size() int
BitArray() *bits.BitArray
GetByIndex(int32) *Vote
IsCommit() bool
}
+559
View File
@@ -0,0 +1,559 @@
package block
import (
"bytes"
"context"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmtime "github.com/tendermint/tendermint/libs/time"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
func TestVoteSet_AddVote_Good(t *testing.T) {
height, round := int64(1), int32(0)
voteSet, _, privValidators := randVoteSet(height, round, tmproto.PrevoteType, 10, 1)
val0 := privValidators[0]
val0p, err := val0.GetPubKey(context.Background())
require.NoError(t, err)
val0Addr := val0p.Address()
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")
vote := &Vote{
ValidatorAddress: val0Addr,
ValidatorIndex: 0, // since privValidators are in order
Height: height,
Round: round,
Type: tmproto.PrevoteType,
Timestamp: tmtime.Now(),
BlockID: BlockID{nil, PartSetHeader{}},
}
_, err = signAddVote(val0, vote, voteSet)
require.NoError(t, err)
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")
}
func TestVoteSet_AddVote_Bad(t *testing.T) {
height, round := int64(1), int32(0)
voteSet, _, privValidators := randVoteSet(height, round, tmproto.PrevoteType, 10, 1)
voteProto := &Vote{
ValidatorAddress: nil,
ValidatorIndex: -1,
Height: height,
Round: round,
Timestamp: tmtime.Now(),
Type: tmproto.PrevoteType,
BlockID: BlockID{nil, PartSetHeader{}},
}
// val0 votes for nil.
{
pubKey, err := privValidators[0].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 0)
added, err := signAddVote(privValidators[0], vote, voteSet)
if !added || err != nil {
t.Errorf("expected VoteSet.Add to succeed")
}
}
// val0 votes again for some block.
{
pubKey, err := privValidators[0].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 0)
added, err := signAddVote(privValidators[0], withBlockHash(vote, tmrand.Bytes(32)), voteSet)
if added || err == nil {
t.Errorf("expected VoteSet.Add to fail, conflicting vote.")
}
}
// val1 votes on another height
{
pubKey, err := privValidators[1].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 1)
added, err := signAddVote(privValidators[1], withHeight(vote, height+1), voteSet)
if added || err == nil {
t.Errorf("expected VoteSet.Add to fail, wrong height")
}
}
// val2 votes on another round
{
pubKey, err := privValidators[2].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 2)
added, err := signAddVote(privValidators[2], withRound(vote, round+1), voteSet)
if added || err == nil {
t.Errorf("expected VoteSet.Add to fail, wrong round")
}
}
// val3 votes of another type.
{
pubKey, err := privValidators[3].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 3)
added, err := signAddVote(privValidators[3], withType(vote, byte(tmproto.PrecommitType)), voteSet)
if added || err == nil {
t.Errorf("expected VoteSet.Add to fail, wrong type")
}
}
}
func TestVoteSet_2_3Majority(t *testing.T) {
height, round := int64(1), int32(0)
voteSet, _, privValidators := randVoteSet(height, round, tmproto.PrevoteType, 10, 1)
voteProto := &Vote{
ValidatorAddress: nil, // NOTE: must fill in
ValidatorIndex: -1, // NOTE: must fill in
Height: height,
Round: round,
Type: tmproto.PrevoteType,
Timestamp: tmtime.Now(),
BlockID: BlockID{nil, PartSetHeader{}},
}
// 6 out of 10 voted for nil.
for i := int32(0); i < 6; i++ {
pubKey, err := privValidators[i].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, i)
_, err = signAddVote(privValidators[i], vote, voteSet)
require.NoError(t, err)
}
blockID, ok := voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(), "there should be no 2/3 majority")
// 7th validator voted for some blockhash
{
pubKey, err := privValidators[6].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 6)
_, 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")
}
// 8th validator voted for nil.
{
pubKey, err := privValidators[7].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 7)
_, 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")
}
}
func TestVoteSet_2_3MajorityRedux(t *testing.T) {
height, round := int64(1), int32(0)
voteSet, _, privValidators := randVoteSet(height, round, tmproto.PrevoteType, 100, 1)
blockHash := crypto.CRandBytes(32)
blockPartsTotal := uint32(123)
blockPartSetHeader := PartSetHeader{blockPartsTotal, crypto.CRandBytes(32)}
voteProto := &Vote{
ValidatorAddress: nil, // NOTE: must fill in
ValidatorIndex: -1, // NOTE: must fill in
Height: height,
Round: round,
Timestamp: tmtime.Now(),
Type: tmproto.PrevoteType,
BlockID: BlockID{blockHash, blockPartSetHeader},
}
// 66 out of 100 voted for nil.
for i := int32(0); i < 66; i++ {
pubKey, err := privValidators[i].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, i)
_, err = signAddVote(privValidators[i], vote, voteSet)
require.NoError(t, err)
}
blockID, ok := voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
"there should be no 2/3 majority")
// 67th validator voted for nil
{
pubKey, err := privValidators[66].GetPubKey(context.Background())
require.NoError(t, err)
adrr := pubKey.Address()
vote := withValidator(voteProto, adrr, 66)
_, err = signAddVote(privValidators[66], withBlockHash(vote, nil), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
"there should be no 2/3 majority: last vote added was nil")
}
// 68th validator voted for a different BlockParts PartSetHeader
{
pubKey, err := privValidators[67].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 67)
blockPartsHeader := PartSetHeader{blockPartsTotal, crypto.CRandBytes(32)}
_, err = signAddVote(privValidators[67], withBlockPartSetHeader(vote, blockPartsHeader), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
"there should be no 2/3 majority: last vote added had different PartSetHeader Hash")
}
// 69th validator voted for different BlockParts Total
{
pubKey, err := privValidators[68].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 68)
blockPartsHeader := PartSetHeader{blockPartsTotal + 1, blockPartSetHeader.Hash}
_, err = signAddVote(privValidators[68], withBlockPartSetHeader(vote, blockPartsHeader), voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.False(t, ok || !blockID.IsZero(),
"there should be no 2/3 majority: last vote added had different PartSetHeader Total")
}
// 70th validator voted for different BlockHash
{
pubKey, err := privValidators[69].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 69)
_, err = signAddVote(privValidators[69], 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: last vote added had different BlockHash")
}
// 71st validator voted for the right BlockHash & BlockPartSetHeader
{
pubKey, err := privValidators[70].GetPubKey(context.Background())
require.NoError(t, err)
addr := pubKey.Address()
vote := withValidator(voteProto, addr, 70)
_, err = signAddVote(privValidators[70], vote, voteSet)
require.NoError(t, err)
blockID, ok = voteSet.TwoThirdsMajority()
assert.True(t, ok && blockID.Equals(BlockID{blockHash, blockPartSetHeader}),
"there should be 2/3 majority")
}
}
func TestVoteSet_Conflicts(t *testing.T) {
height, round := int64(1), int32(0)
voteSet, _, privValidators := randVoteSet(height, round, tmproto.PrevoteType, 4, 1)
blockHash1 := tmrand.Bytes(32)
blockHash2 := tmrand.Bytes(32)
voteProto := &Vote{
ValidatorAddress: nil,
ValidatorIndex: -1,
Height: height,
Round: round,
Timestamp: tmtime.Now(),
Type: tmproto.PrevoteType,
BlockID: BlockID{nil, PartSetHeader{}},
}
val0, err := privValidators[0].GetPubKey(context.Background())
require.NoError(t, err)
val0Addr := val0.Address()
// val0 votes for nil.
{
vote := withValidator(voteProto, val0Addr, 0)
added, err := signAddVote(privValidators[0], vote, voteSet)
if !added || err != nil {
t.Errorf("expected VoteSet.Add to succeed")
}
}
// val0 votes again for blockHash1.
{
vote := withValidator(voteProto, val0Addr, 0)
added, err := signAddVote(privValidators[0], withBlockHash(vote, blockHash1), voteSet)
assert.False(t, added, "conflicting vote")
assert.Error(t, err, "conflicting vote")
}
// start tracking blockHash1
err = voteSet.SetPeerMaj23("peerA", BlockID{blockHash1, PartSetHeader{}})
require.NoError(t, err)
// val0 votes again for blockHash1.
{
vote := withValidator(voteProto, val0Addr, 0)
added, err := signAddVote(privValidators[0], withBlockHash(vote, blockHash1), voteSet)
assert.True(t, added, "called SetPeerMaj23()")
assert.Error(t, err, "conflicting vote")
}
// attempt tracking blockHash2, should fail because already set for peerA.
err = voteSet.SetPeerMaj23("peerA", BlockID{blockHash2, PartSetHeader{}})
require.Error(t, err)
// val0 votes again for blockHash1.
{
vote := withValidator(voteProto, val0Addr, 0)
added, err := signAddVote(privValidators[0], withBlockHash(vote, blockHash2), voteSet)
assert.False(t, added, "duplicate SetPeerMaj23() from peerA")
assert.Error(t, err, "conflicting vote")
}
// val1 votes for blockHash1.
{
pv, err := privValidators[1].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, 1)
added, err := signAddVote(privValidators[1], withBlockHash(vote, blockHash1), voteSet)
if !added || err != nil {
t.Errorf("expected VoteSet.Add to succeed")
}
}
// check
if voteSet.HasTwoThirdsMajority() {
t.Errorf("we shouldn't have 2/3 majority yet")
}
if voteSet.HasTwoThirdsAny() {
t.Errorf("we shouldn't have 2/3 if any votes yet")
}
// val2 votes for blockHash2.
{
pv, err := privValidators[2].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, 2)
added, err := signAddVote(privValidators[2], withBlockHash(vote, blockHash2), voteSet)
if !added || err != nil {
t.Errorf("expected VoteSet.Add to succeed")
}
}
// check
if voteSet.HasTwoThirdsMajority() {
t.Errorf("we shouldn't have 2/3 majority yet")
}
if !voteSet.HasTwoThirdsAny() {
t.Errorf("we should have 2/3 if any votes")
}
// now attempt tracking blockHash1
err = voteSet.SetPeerMaj23("peerB", BlockID{blockHash1, PartSetHeader{}})
require.NoError(t, err)
// val2 votes for blockHash1.
{
pv, err := privValidators[2].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, 2)
added, err := signAddVote(privValidators[2], withBlockHash(vote, blockHash1), voteSet)
assert.True(t, added)
assert.Error(t, err, "conflicting vote")
}
// check
if !voteSet.HasTwoThirdsMajority() {
t.Errorf("we should have 2/3 majority for blockHash1")
}
blockIDMaj23, _ := voteSet.TwoThirdsMajority()
if !bytes.Equal(blockIDMaj23.Hash, blockHash1) {
t.Errorf("got the wrong 2/3 majority blockhash")
}
if !voteSet.HasTwoThirdsAny() {
t.Errorf("we should have 2/3 if any votes")
}
}
func TestVoteSet_MakeCommit(t *testing.T) {
height, round := int64(1), int32(0)
voteSet, _, privValidators := randVoteSet(height, round, tmproto.PrecommitType, 10, 1)
blockHash, blockPartSetHeader := crypto.CRandBytes(32), PartSetHeader{123, crypto.CRandBytes(32)}
voteProto := &Vote{
ValidatorAddress: nil,
ValidatorIndex: -1,
Height: height,
Round: round,
Timestamp: tmtime.Now(),
Type: tmproto.PrecommitType,
BlockID: BlockID{blockHash, blockPartSetHeader},
}
// 6 out of 10 voted for some block.
for i := int32(0); i < 6; i++ {
pv, err := privValidators[i].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, i)
_, err = signAddVote(privValidators[i], vote, voteSet)
if err != nil {
t.Error(err)
}
}
// MakeCommit should fail.
assert.Panics(t, func() { voteSet.MakeCommit() }, "Doesn't have +2/3 majority")
// 7th voted for some other block.
{
pv, err := privValidators[6].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, 6)
vote = withBlockHash(vote, tmrand.Bytes(32))
vote = withBlockPartSetHeader(vote, PartSetHeader{123, tmrand.Bytes(32)})
_, err = signAddVote(privValidators[6], vote, voteSet)
require.NoError(t, err)
}
// The 8th voted like everyone else.
{
pv, err := privValidators[7].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, 7)
_, err = signAddVote(privValidators[7], vote, voteSet)
require.NoError(t, err)
}
// The 9th voted for nil.
{
pv, err := privValidators[8].GetPubKey(context.Background())
assert.NoError(t, err)
addr := pv.Address()
vote := withValidator(voteProto, addr, 8)
vote.BlockID = BlockID{}
_, err = signAddVote(privValidators[8], vote, voteSet)
require.NoError(t, err)
}
commit := voteSet.MakeCommit()
// Commit should have 10 elements
assert.Equal(t, 10, len(commit.Signatures))
// Ensure that Commit is good.
if err := commit.ValidateBasic(); err != nil {
t.Errorf("error in Commit.ValidateBasic(): %v", err)
}
}
// NOTE: privValidators are in order
func randVoteSet(
height int64,
round int32,
signedMsgType tmproto.SignedMsgType,
numValidators int,
votingPower int64,
) (*VoteSet, *ValidatorSet, []PrivValidator) {
valSet, privValidators := randValidatorPrivValSet(numValidators, votingPower)
return NewVoteSet("test_chain_id", height, round, signedMsgType, valSet), valSet, privValidators
}
func deterministicVoteSet(
height int64,
round int32,
signedMsgType tmproto.SignedMsgType,
votingPower int64,
) (*VoteSet, *ValidatorSet, []PrivValidator) {
valSet, privValidators := deterministicValidatorSet()
return NewVoteSet("test_chain_id", height, round, signedMsgType, valSet), valSet, privValidators
}
func randValidatorPrivValSet(numValidators int, votingPower int64) (*ValidatorSet, []PrivValidator) {
var (
valz = make([]*Validator, numValidators)
privValidators = make([]PrivValidator, numValidators)
)
for i := 0; i < numValidators; i++ {
val, privValidator := randValidator(false, votingPower)
valz[i] = val
privValidators[i] = privValidator
}
sort.Sort(PrivValidatorsByAddress(privValidators))
return NewValidatorSet(valz), privValidators
}
// Convenience: Return new vote with different validator address/index
func withValidator(vote *Vote, addr []byte, idx int32) *Vote {
vote = vote.Copy()
vote.ValidatorAddress = addr
vote.ValidatorIndex = idx
return vote
}
// Convenience: Return new vote with different height
func withHeight(vote *Vote, height int64) *Vote {
vote = vote.Copy()
vote.Height = height
return vote
}
// Convenience: Return new vote with different round
func withRound(vote *Vote, round int32) *Vote {
vote = vote.Copy()
vote.Round = round
return vote
}
// Convenience: Return new vote with different type
func withType(vote *Vote, signedMsgType byte) *Vote {
vote = vote.Copy()
vote.Type = tmproto.SignedMsgType(signedMsgType)
return vote
}
// Convenience: Return new vote with different blockHash
func withBlockHash(vote *Vote, blockHash []byte) *Vote {
vote = vote.Copy()
vote.BlockID.Hash = blockHash
return vote
}
// Convenience: Return new vote with different blockParts
func withBlockPartSetHeader(vote *Vote, blockPartsHeader PartSetHeader) *Vote {
vote = vote.Copy()
vote.BlockID.PartSetHeader = blockPartsHeader
return vote
}
+296
View File
@@ -0,0 +1,296 @@
package block
import (
"context"
"testing"
"time"
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/internal/libs/protoio"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
func examplePrevote() *Vote {
return exampleVote(byte(tmproto.PrevoteType))
}
func examplePrecommit() *Vote {
return exampleVote(byte(tmproto.PrecommitType))
}
func exampleVote(t byte) *Vote {
var stamp, err = time.Parse(TimeFormat, "2017-12-25T03:00:01.234Z")
if err != nil {
panic(err)
}
return &Vote{
Type: tmproto.SignedMsgType(t),
Height: 12345,
Round: 2,
Timestamp: stamp,
BlockID: BlockID{
Hash: tmhash.Sum([]byte("blockID_hash")),
PartSetHeader: PartSetHeader{
Total: 1000000,
Hash: tmhash.Sum([]byte("blockID_part_set_header_hash")),
},
},
ValidatorAddress: crypto.AddressHash([]byte("validator_address")),
ValidatorIndex: 56789,
}
}
func TestVoteSignable(t *testing.T) {
vote := examplePrecommit()
v := vote.ToProto()
signBytes := VoteSignBytes("test_chain_id", v)
pb := CanonicalizeVote("test_chain_id", v)
expected, err := protoio.MarshalDelimited(&pb)
require.NoError(t, err)
require.Equal(t, expected, signBytes, "Got unexpected sign bytes for Vote.")
}
func TestVoteSignBytesTestVectors(t *testing.T) {
tests := []struct {
chainID string
vote *Vote
want []byte
}{
0: {
"", &Vote{},
// NOTE: Height and Round are skipped here. This case needs to be considered while parsing.
[]byte{0xd, 0x2a, 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
},
// with proper (fixed size) height and round (PreCommit):
1: {
"", &Vote{Height: 1, Round: 1, Type: tmproto.PrecommitType},
[]byte{
0x21, // length
0x8, // (field_number << 3) | wire_type
0x2, // PrecommitType
0x11, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
0x19, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
0x2a, // (field_number << 3) | wire_type
// remaining fields (timestamp):
0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
},
// with proper (fixed size) height and round (PreVote):
2: {
"", &Vote{Height: 1, Round: 1, Type: tmproto.PrevoteType},
[]byte{
0x21, // length
0x8, // (field_number << 3) | wire_type
0x1, // PrevoteType
0x11, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
0x19, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
0x2a, // (field_number << 3) | wire_type
// remaining fields (timestamp):
0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
},
3: {
"", &Vote{Height: 1, Round: 1},
[]byte{
0x1f, // length
0x11, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
0x19, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
// remaining fields (timestamp):
0x2a,
0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
},
// containing non-empty chain_id:
4: {
"test_chain_id", &Vote{Height: 1, Round: 1},
[]byte{
0x2e, // length
0x11, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
0x19, // (field_number << 3) | wire_type
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
// remaining fields:
0x2a, // (field_number << 3) | wire_type
0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, // timestamp
// (field_number << 3) | wire_type
0x32,
0xd, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64}, // chainID
},
}
for i, tc := range tests {
v := tc.vote.ToProto()
got := VoteSignBytes(tc.chainID, v)
assert.Equal(t, len(tc.want), len(got), "test case #%v: got unexpected sign bytes length for Vote.", i)
assert.Equal(t, tc.want, got, "test case #%v: got unexpected sign bytes for Vote.", i)
}
}
func TestVoteProposalNotEq(t *testing.T) {
cv := CanonicalizeVote("", &tmproto.Vote{Height: 1, Round: 1})
p := CanonicalizeProposal("", &tmproto.Proposal{Height: 1, Round: 1})
vb, err := proto.Marshal(&cv)
require.NoError(t, err)
pb, err := proto.Marshal(&p)
require.NoError(t, err)
require.NotEqual(t, vb, pb)
}
func TestVoteVerifySignature(t *testing.T) {
privVal := NewMockPV()
pubkey, err := privVal.GetPubKey(context.Background())
require.NoError(t, err)
vote := examplePrecommit()
v := vote.ToProto()
signBytes := VoteSignBytes("test_chain_id", v)
// sign it
err = privVal.SignVote(context.Background(), "test_chain_id", v)
require.NoError(t, err)
// verify the same vote
valid := pubkey.VerifySignature(VoteSignBytes("test_chain_id", v), v.Signature)
require.True(t, valid)
// serialize, deserialize and verify again....
precommit := new(tmproto.Vote)
bs, err := proto.Marshal(v)
require.NoError(t, err)
err = proto.Unmarshal(bs, precommit)
require.NoError(t, err)
// verify the transmitted vote
newSignBytes := VoteSignBytes("test_chain_id", precommit)
require.Equal(t, string(signBytes), string(newSignBytes))
valid = pubkey.VerifySignature(newSignBytes, precommit.Signature)
require.True(t, valid)
}
func TestIsVoteTypeValid(t *testing.T) {
tc := []struct {
name string
in tmproto.SignedMsgType
out bool
}{
{"Prevote", tmproto.PrevoteType, true},
{"Precommit", tmproto.PrecommitType, true},
{"InvalidType", tmproto.SignedMsgType(0x3), false},
}
for _, tt := range tc {
tt := tt
t.Run(tt.name, func(st *testing.T) {
if rs := IsVoteTypeValid(tt.in); rs != tt.out {
t.Errorf("got unexpected Vote type. Expected:\n%v\nGot:\n%v", rs, tt.out)
}
})
}
}
func TestVoteVerify(t *testing.T) {
privVal := NewMockPV()
pubkey, err := privVal.GetPubKey(context.Background())
require.NoError(t, err)
vote := examplePrevote()
vote.ValidatorAddress = pubkey.Address()
err = vote.Verify("test_chain_id", ed25519.GenPrivKey().PubKey())
if assert.Error(t, err) {
assert.Equal(t, ErrVoteInvalidValidatorAddress, err)
}
err = vote.Verify("test_chain_id", pubkey)
if assert.Error(t, err) {
assert.Equal(t, ErrVoteInvalidSignature, err)
}
}
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
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
if str2 != expected {
t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str2)
}
}
func TestVoteValidateBasic(t *testing.T) {
privVal := NewMockPV()
testCases := []struct {
testName string
malleateVote func(*Vote)
expectErr bool
}{
{"Good Vote", func(v *Vote) {}, false},
{"Negative Height", func(v *Vote) { v.Height = -1 }, true},
{"Negative Round", func(v *Vote) { v.Round = -1 }, true},
{"Invalid BlockID", func(v *Vote) {
v.BlockID = BlockID{[]byte{1, 2, 3}, PartSetHeader{111, []byte("blockparts")}}
}, true},
{"Invalid Address", func(v *Vote) { v.ValidatorAddress = make([]byte, 1) }, true},
{"Invalid ValidatorIndex", func(v *Vote) { v.ValidatorIndex = -1 }, true},
{"Invalid Signature", func(v *Vote) { v.Signature = nil }, true},
{"Too big Signature", func(v *Vote) { v.Signature = make([]byte, MaxSignatureSize+1) }, true},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
vote := examplePrecommit()
v := vote.ToProto()
err := privVal.SignVote(context.Background(), "test_chain_id", v)
vote.Signature = v.Signature
require.NoError(t, err)
tc.malleateVote(vote)
assert.Equal(t, tc.expectErr, vote.ValidateBasic() != nil, "Validate Basic had an unexpected result")
})
}
}
func TestVoteProtobuf(t *testing.T) {
privVal := NewMockPV()
vote := examplePrecommit()
v := vote.ToProto()
err := privVal.SignVote(context.Background(), "test_chain_id", v)
vote.Signature = v.Signature
require.NoError(t, err)
testCases := []struct {
msg string
v1 *Vote
expPass bool
}{
{"success", vote, true},
{"fail vote validate basic", &Vote{}, false},
{"failure nil", nil, false},
}
for _, tc := range testCases {
protoProposal := tc.v1.ToProto()
v, err := VoteFromProto(protoProposal)
if tc.expPass {
require.NoError(t, err)
require.Equal(t, tc.v1, v, tc.msg)
} else {
require.Error(t, err)
}
}
}