package types 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(t *testing.T) *Vote { t.Helper() return exampleVote(t, byte(tmproto.PrevoteType)) } func examplePrecommit(t testing.TB) *Vote { t.Helper() return exampleVote(t, byte(tmproto.PrecommitType)) } func exampleVote(tb testing.TB, t byte) *Vote { tb.Helper() var stamp, err = time.Parse(TimeFormat, "2017-12-25T03:00:01.234Z") require.NoError(tb, 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(t) 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 }, // containing vote extension 5: { "test_chain_id", &Vote{ Type: 0, Height: 1, Round: 1, BlockID: BlockID{}, Timestamp: time.Time{}, ValidatorIndex: 0, Extension: []byte("signed"), }, []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 // remaning 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 // (field_number << 3) | wire_type }, // 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) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() privVal := NewMockPV() pubkey, err := privVal.GetPubKey(ctx) require.NoError(t, err) vote := examplePrecommit(t) v := vote.ToProto() signBytes := VoteSignBytes("test_chain_id", v) // sign it err = privVal.SignVote(ctx, "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) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() privVal := NewMockPV() pubkey, err := privVal.GetPubKey(ctx) require.NoError(t, err) vote := examplePrevote(t) 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(t).String() expected := `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PRECOMMIT(Precommit) 8B01023386C3 000000000000 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(t).String() expected = `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PREVOTE(Prevote) 8B01023386C3 000000000000 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) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() vote := examplePrecommit(t) v := vote.ToProto() err := privVal.SignVote(ctx, "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) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() privVal := NewMockPV() vote := examplePrecommit(t) v := vote.ToProto() err := privVal.SignVote(ctx, "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) } } } var sink interface{} func getSampleCommit(ctx context.Context, t testing.TB) *Commit { t.Helper() lastID := makeBlockIDRandom() voteSet, _, vals := randVoteSet(ctx, t, 2, 1, tmproto.PrecommitType, 10, 1) commit, err := makeCommit(ctx, lastID, 2, 1, voteSet, vals, time.Now()) require.NoError(t, err) return commit } func BenchmarkVoteSignBytes(b *testing.B) { protoVote := examplePrecommit(b).ToProto() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { sink = VoteSignBytes("test_chain_id", protoVote) } if sink == nil { b.Fatal("Benchmark did not run") } // Reset the sink. sink = (interface{})(nil) } func BenchmarkCommitVoteSignBytes(b *testing.B) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() sampleCommit := getSampleCommit(ctx, b) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { for index := range sampleCommit.Signatures { sink = sampleCommit.VoteSignBytes("test_chain_id", int32(index)) } } if sink == nil { b.Fatal("Benchmark did not run") } // Reset the sink. sink = (interface{})(nil) }