package types import ( // it is ok to use math/rand here: we do not need a cryptographically secure random // number generator here and we can run the tests a bit faster "crypto/rand" "encoding/hex" "math" "os" "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/merkle" "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/bits" "github.com/tendermint/tendermint/libs/bytes" tmrand "github.com/tendermint/tendermint/libs/rand" tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" ) func TestMain(m *testing.M) { RegisterMockEvidences(cdc) code := m.Run() os.Exit(code) } func TestBlockAddEvidence(t *testing.T) { txs := []Tx{Tx("foo"), Tx("bar")} lastID := makeBlockIDRandom() h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), valSet.Validators[0].Address) evList := []Evidence{ev} block := MakeBlock(h, txs, commit, evList) require.NotNil(t, block) require.Equal(t, 1, len(block.Evidence.Evidence)) require.NotNil(t, block.EvidenceHash) } func TestBlockValidateBasic(t *testing.T) { require.Error(t, (*Block)(nil).ValidateBasic()) txs := []Tx{Tx("foo"), Tx("bar")} lastID := makeBlockIDRandom() h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), valSet.Validators[0].Address) evList := []Evidence{ev} testCases := []struct { testName string malleateBlock func(*Block) expErr bool }{ {"Make Block", func(blk *Block) {}, false}, {"Make Block w/ proposer Addr", func(blk *Block) { blk.ProposerAddress = valSet.GetProposer().Address }, false}, {"Negative Height", func(blk *Block) { blk.Height = -1 }, true}, {"Remove 1/2 the commits", func(blk *Block) { blk.LastCommit.Signatures = commit.Signatures[:commit.Size()/2] blk.LastCommit.hash = nil // clear hash or change wont be noticed }, true}, {"Remove LastCommitHash", func(blk *Block) { blk.LastCommitHash = []byte("something else") }, true}, {"Tampered Data", func(blk *Block) { blk.Data.Txs[0] = Tx("something else") blk.Data.hash = nil // clear hash or change wont be noticed }, true}, {"Tampered DataHash", func(blk *Block) { blk.DataHash = tmrand.Bytes(len(blk.DataHash)) }, true}, {"Tampered EvidenceHash", func(blk *Block) { blk.EvidenceHash = []byte("something else") }, true}, } for i, tc := range testCases { tc := tc i := i t.Run(tc.testName, func(t *testing.T) { block := MakeBlock(h, txs, commit, evList) block.ProposerAddress = valSet.GetProposer().Address tc.malleateBlock(block) err = block.ValidateBasic() assert.Equal(t, tc.expErr, err != nil, "#%d: %v", i, err) }) } } func TestBlockHash(t *testing.T) { assert.Nil(t, (*Block)(nil).Hash()) assert.Nil(t, MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil, nil).Hash()) } func TestBlockMakePartSet(t *testing.T) { assert.Nil(t, (*Block)(nil).MakePartSet(2)) partSet := MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil, nil).MakePartSet(1024) assert.NotNil(t, partSet) assert.Equal(t, 1, partSet.Total()) } func TestBlockMakePartSetWithEvidence(t *testing.T) { assert.Nil(t, (*Block)(nil).MakePartSet(2)) lastID := makeBlockIDRandom() h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), valSet.Validators[0].Address) evList := []Evidence{ev} partSet := MakeBlock(h, []Tx{Tx("Hello World")}, commit, evList).MakePartSet(512) assert.NotNil(t, partSet) assert.Equal(t, 3, partSet.Total()) } func TestBlockHashesTo(t *testing.T) { assert.False(t, (*Block)(nil).HashesTo(nil)) lastID := makeBlockIDRandom() h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), valSet.Validators[0].Address) evList := []Evidence{ev} block := MakeBlock(h, []Tx{Tx("Hello World")}, commit, evList) block.ValidatorsHash = valSet.Hash() assert.False(t, block.HashesTo([]byte{})) assert.False(t, block.HashesTo([]byte("something else"))) assert.True(t, block.HashesTo(block.Hash())) } func TestBlockSize(t *testing.T) { size := MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil, nil).Size() if size <= 0 { t.Fatal("Size of the block is zero or negative") } } func TestBlockString(t *testing.T) { assert.Equal(t, "nil-Block", (*Block)(nil).String()) assert.Equal(t, "nil-Block", (*Block)(nil).StringIndented("")) assert.Equal(t, "nil-Block", (*Block)(nil).StringShort()) block := MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil, nil) assert.NotEqual(t, "nil-Block", block.String()) assert.NotEqual(t, "nil-Block", block.StringIndented("")) assert.NotEqual(t, "nil-Block", block.StringShort()) } func makeBlockIDRandom() BlockID { var ( blockHash = make([]byte, tmhash.Size) partSetHash = make([]byte, tmhash.Size) ) rand.Read(blockHash) //nolint: gosec rand.Read(partSetHash) //nolint: gosec return BlockID{blockHash, PartSetHeader{123, partSetHash}} } func makeBlockID(hash []byte, partSetSize int, partSetHash []byte) BlockID { var ( h = make([]byte, tmhash.Size) psH = make([]byte, tmhash.Size) ) copy(h, hash) copy(psH, partSetHash) return BlockID{ Hash: h, PartsHeader: PartSetHeader{ Total: partSetSize, Hash: psH, }, } } var nilBytes []byte func TestNilHeaderHashDoesntCrash(t *testing.T) { assert.Equal(t, []byte((*Header)(nil).Hash()), nilBytes) assert.Equal(t, []byte((new(Header)).Hash()), nilBytes) } func TestNilDataHashDoesntCrash(t *testing.T) { assert.Equal(t, []byte((*Data)(nil).Hash()), nilBytes) assert.Equal(t, []byte(new(Data).Hash()), nilBytes) } func TestCommit(t *testing.T) { lastID := makeBlockIDRandom() h := int64(3) voteSet, _, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) assert.Equal(t, h-1, commit.Height) assert.Equal(t, 1, commit.Round) assert.Equal(t, PrecommitType, SignedMsgType(commit.Type())) if commit.Size() <= 0 { t.Fatalf("commit %v has a zero or negative size: %d", commit, commit.Size()) } require.NotNil(t, commit.BitArray()) assert.Equal(t, bits.NewBitArray(10).Size(), commit.BitArray().Size()) assert.Equal(t, voteSet.GetByIndex(0), commit.GetByIndex(0)) assert.True(t, commit.IsCommit()) } func TestCommitValidateBasic(t *testing.T) { testCases := []struct { testName string malleateCommit func(*Commit) expectErr bool }{ {"Random Commit", func(com *Commit) {}, false}, {"Incorrect signature", func(com *Commit) { com.Signatures[0].Signature = []byte{0} }, false}, {"Incorrect height", func(com *Commit) { com.Height = int64(-100) }, true}, {"Incorrect round", func(com *Commit) { com.Round = -100 }, true}, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { com := randCommit(time.Now()) tc.malleateCommit(com) assert.Equal(t, tc.expectErr, com.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } } func TestHeaderHash(t *testing.T) { testCases := []struct { desc string header *Header expectHash bytes.HexBytes }{ {"Generates expected hash", &Header{ Version: version.Consensus{Block: 1, App: 2}, ChainID: "chainId", Height: 3, Time: time.Date(2019, 10, 13, 16, 14, 44, 0, time.UTC), LastBlockID: makeBlockID(make([]byte, tmhash.Size), 6, make([]byte, tmhash.Size)), LastCommitHash: tmhash.Sum([]byte("last_commit_hash")), DataHash: tmhash.Sum([]byte("data_hash")), ValidatorsHash: tmhash.Sum([]byte("validators_hash")), NextValidatorsHash: tmhash.Sum([]byte("next_validators_hash")), ConsensusHash: tmhash.Sum([]byte("consensus_hash")), AppHash: tmhash.Sum([]byte("app_hash")), LastResultsHash: tmhash.Sum([]byte("last_results_hash")), EvidenceHash: tmhash.Sum([]byte("evidence_hash")), ProposerAddress: crypto.AddressHash([]byte("proposer_address")), }, hexBytesFromString("ABDC78921B18A47EE6BEF5E31637BADB0F3E587E3C0F4DB2D1E93E9FF0533862")}, {"nil header yields nil", nil, nil}, {"nil ValidatorsHash yields nil", &Header{ Version: version.Consensus{Block: 1, App: 2}, ChainID: "chainId", Height: 3, Time: time.Date(2019, 10, 13, 16, 14, 44, 0, time.UTC), LastBlockID: makeBlockID(make([]byte, tmhash.Size), 6, make([]byte, tmhash.Size)), LastCommitHash: tmhash.Sum([]byte("last_commit_hash")), DataHash: tmhash.Sum([]byte("data_hash")), ValidatorsHash: nil, NextValidatorsHash: tmhash.Sum([]byte("next_validators_hash")), ConsensusHash: tmhash.Sum([]byte("consensus_hash")), AppHash: tmhash.Sum([]byte("app_hash")), LastResultsHash: tmhash.Sum([]byte("last_results_hash")), EvidenceHash: tmhash.Sum([]byte("evidence_hash")), ProposerAddress: crypto.AddressHash([]byte("proposer_address")), }, nil}, } for _, tc := range testCases { tc := tc t.Run(tc.desc, func(t *testing.T) { assert.Equal(t, tc.expectHash, tc.header.Hash()) // We also make sure that all fields are hashed in struct order, and that all // fields in the test struct are non-zero. if tc.header != nil && tc.expectHash != nil { byteSlices := [][]byte{} s := reflect.ValueOf(*tc.header) for i := 0; i < s.NumField(); i++ { f := s.Field(i) assert.False(t, f.IsZero(), "Found zero-valued field %v", s.Type().Field(i).Name) byteSlices = append(byteSlices, cdcEncode(f.Interface())) } assert.Equal(t, bytes.HexBytes(merkle.SimpleHashFromByteSlices(byteSlices)), tc.header.Hash()) } }) } } func TestMaxHeaderBytes(t *testing.T) { // Construct a UTF-8 string of MaxChainIDLen length using the supplementary // characters. // Each supplementary character takes 4 bytes. // http://www.i18nguy.com/unicode/supplementary-test.html maxChainID := "" for i := 0; i < MaxChainIDLen; i++ { maxChainID += "𠜎" } // time is varint encoded so need to pick the max. // year int, month Month, day, hour, min, sec, nsec int, loc *Location timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC) h := Header{ Version: version.Consensus{Block: math.MaxInt64, App: math.MaxInt64}, ChainID: maxChainID, Height: math.MaxInt64, Time: timestamp, LastBlockID: makeBlockID(make([]byte, tmhash.Size), math.MaxInt64, make([]byte, tmhash.Size)), LastCommitHash: tmhash.Sum([]byte("last_commit_hash")), DataHash: tmhash.Sum([]byte("data_hash")), ValidatorsHash: tmhash.Sum([]byte("validators_hash")), NextValidatorsHash: tmhash.Sum([]byte("next_validators_hash")), ConsensusHash: tmhash.Sum([]byte("consensus_hash")), AppHash: tmhash.Sum([]byte("app_hash")), LastResultsHash: tmhash.Sum([]byte("last_results_hash")), EvidenceHash: tmhash.Sum([]byte("evidence_hash")), ProposerAddress: crypto.AddressHash([]byte("proposer_address")), } bz, err := cdc.MarshalBinaryLengthPrefixed(h) require.NoError(t, err) assert.EqualValues(t, MaxHeaderBytes, int64(len(bz))) } func randCommit(now time.Time) *Commit { lastID := makeBlockIDRandom() h := int64(3) voteSet, _, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, now) if err != nil { panic(err) } return commit } func hexBytesFromString(s string) bytes.HexBytes { b, err := hex.DecodeString(s) if err != nil { panic(err) } return bytes.HexBytes(b) } func TestBlockMaxDataBytes(t *testing.T) { testCases := []struct { maxBytes int64 valsCount int evidenceCount int panics bool result int64 }{ 0: {-10, 1, 0, true, 0}, 1: {10, 1, 0, true, 0}, 2: {865, 1, 0, true, 0}, 3: {866, 1, 0, false, 0}, 4: {867, 1, 0, false, 1}, } for i, tc := range testCases { tc := tc if tc.panics { assert.Panics(t, func() { MaxDataBytes(tc.maxBytes, tc.valsCount, tc.evidenceCount) }, "#%v", i) } else { assert.Equal(t, tc.result, MaxDataBytes(tc.maxBytes, tc.valsCount, tc.evidenceCount), "#%v", i) } } } func TestBlockMaxDataBytesUnknownEvidence(t *testing.T) { testCases := []struct { maxBytes int64 valsCount int panics bool result int64 }{ 0: {-10, 1, true, 0}, 1: {10, 1, true, 0}, 2: {961, 1, true, 0}, 3: {962, 1, false, 0}, 4: {963, 1, false, 1}, } for i, tc := range testCases { tc := tc if tc.panics { assert.Panics(t, func() { MaxDataBytesUnknownEvidence(tc.maxBytes, tc.valsCount) }, "#%v", i) } else { assert.Equal(t, tc.result, MaxDataBytesUnknownEvidence(tc.maxBytes, tc.valsCount), "#%v", i) } } } func TestCommitToVoteSet(t *testing.T) { lastID := makeBlockIDRandom() h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) assert.NoError(t, err) chainID := voteSet.ChainID() voteSet2 := CommitToVoteSet(chainID, commit, valSet) for i := 0; i < len(vals); i++ { vote1 := voteSet.GetByIndex(i) vote2 := voteSet2.GetByIndex(i) vote3 := commit.GetVote(i) vote1bz := cdc.MustMarshalBinaryBare(vote1) vote2bz := cdc.MustMarshalBinaryBare(vote2) vote3bz := cdc.MustMarshalBinaryBare(vote3) assert.Equal(t, vote1bz, vote2bz) assert.Equal(t, vote1bz, vote3bz) } } func TestCommitToVoteSetWithVotesForNilBlock(t *testing.T) { blockID := makeBlockID([]byte("blockhash"), 1000, []byte("partshash")) const ( height = int64(3) round = 0 ) type commitVoteTest struct { blockIDs []BlockID numVotes []int // must sum to numValidators numValidators int valid bool } testCases := []commitVoteTest{ {[]BlockID{blockID, {}}, []int{67, 33}, 100, true}, } for _, tc := range testCases { voteSet, valSet, vals := randVoteSet(height-1, round, PrecommitType, tc.numValidators, 1) vi := 0 for n := range tc.blockIDs { for i := 0; i < tc.numVotes[n]; i++ { pubKey, err := vals[vi].GetPubKey() require.NoError(t, err) vote := &Vote{ ValidatorAddress: pubKey.Address(), ValidatorIndex: vi, Height: height - 1, Round: round, Type: PrecommitType, BlockID: tc.blockIDs[n], Timestamp: tmtime.Now(), } added, err := signAddVote(vals[vi], vote, voteSet) assert.NoError(t, err) assert.True(t, added) vi++ } } if tc.valid { commit := voteSet.MakeCommit() // panics without > 2/3 valid votes assert.NotNil(t, commit) err := valSet.VerifyCommit(voteSet.ChainID(), blockID, height-1, commit) assert.Nil(t, err) } else { assert.Panics(t, func() { voteSet.MakeCommit() }) } } } 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: math.MaxInt64, 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, } assert.Equal( t, tc.expectErr, sh.ValidateBasic(validSignedHeader.Header.ChainID) != nil, "Validate Basic had an unexpected result", ) }) } } func TestBlockIDValidateBasic(t *testing.T) { validBlockID := BlockID{ Hash: bytes.HexBytes{}, PartsHeader: PartSetHeader{ Total: 1, Hash: bytes.HexBytes{}, }, } invalidBlockID := BlockID{ Hash: []byte{0}, PartsHeader: PartSetHeader{ Total: -1, Hash: bytes.HexBytes{}, }, } testCases := []struct { testName string blockIDHash bytes.HexBytes blockIDPartsHeader PartSetHeader expectErr bool }{ {"Valid BlockID", validBlockID.Hash, validBlockID.PartsHeader, false}, {"Invalid BlockID", invalidBlockID.Hash, validBlockID.PartsHeader, true}, {"Invalid BlockID", validBlockID.Hash, invalidBlockID.PartsHeader, true}, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { blockID := BlockID{ Hash: tc.blockIDHash, PartsHeader: tc.blockIDPartsHeader, } assert.Equal(t, tc.expectErr, blockID.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } }