// Copyright 2018 The dexon-consensus-core Authors // This file is part of the dexon-consensus-core library. // // The dexon-consensus-core library is free software: you can redistribute it // and/or modify it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation, either version 3 of the License, // or (at your option) any later version. // // The dexon-consensus-core library is distributed in the hope that it will be // useful, but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser // General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the dexon-consensus-core library. If not, see // . // TODO(mission): we should check the return value from processBlock. package core import ( "math/rand" "sort" "testing" "time" "github.com/stretchr/testify/suite" "github.com/dexon-foundation/dexon-consensus-core/blockdb" "github.com/dexon-foundation/dexon-consensus-core/common" "github.com/dexon-foundation/dexon-consensus-core/core/test" "github.com/dexon-foundation/dexon-consensus-core/core/types" ) type ReliableBroadcastTest struct { suite.Suite } func (s *ReliableBroadcastTest) SetupSuite() { } func (s *ReliableBroadcastTest) SetupTest() { } func (s *ReliableBroadcastTest) prepareGenesisBlock( proposerID types.ValidatorID, validatorIDs []types.ValidatorID) (b *types.Block) { b = &types.Block{ ProposerID: proposerID, ParentHash: common.Hash{}, Position: types.Position{ Height: 0, }, Acks: make(map[common.Hash]struct{}), Timestamps: genTimestamps(validatorIDs), } for i, vID := range validatorIDs { if proposerID == vID { b.Position.ChainID = uint32(i) break } } for _, vID := range validatorIDs { b.Timestamps[vID] = time.Time{} } b.Timestamps[proposerID] = time.Now().UTC() var err error b.Hash, err = hashBlock(b) s.Require().Nil(err) return } func genTimestamps(vids []types.ValidatorID) map[types.ValidatorID]time.Time { ts := make(map[types.ValidatorID]time.Time) for _, vid := range vids { ts[vid] = time.Now().UTC() } return ts } // genTestCase1 generates test case 1, // 3 // | // 2 // | \ // 1 | 1 // | | | // 0 0 0 0 (block height) // 0 1 2 3 (validator) func genTestCase1(s *ReliableBroadcastTest, rb *reliableBroadcast) []types.ValidatorID { // Create new reliableBroadcast instance with 4 validators var b *types.Block var h common.Hash vids := []types.ValidatorID{} for i := 0; i < 4; i++ { vid := types.ValidatorID{Hash: common.NewRandomHash()} rb.addValidator(vid) vids = append(vids, vid) } rb.setChainNum(uint32(len(vids))) // Add genesis blocks. for _, vid := range vids { b = s.prepareGenesisBlock(vid, vids) s.Require().Nil(rb.processBlock(b)) } // Add block 0-1 which acks 0-0. h = rb.lattice[0].blocks[0].Hash b = &types.Block{ ProposerID: vids[0], ParentHash: h, Hash: common.NewRandomHash(), Timestamps: genTimestamps(vids), Position: types.Position{ ChainID: 0, Height: 1, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, } var err error b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) s.Require().NotNil(rb.lattice[0].blocks[1]) // Add block 0-2 which acks 0-1 and 1-0. h = rb.lattice[0].blocks[1].Hash b = &types.Block{ ProposerID: vids[0], ParentHash: h, Position: types.Position{ ChainID: 0, Height: 2, }, Timestamps: genTimestamps(vids), Acks: map[common.Hash]struct{}{ h: struct{}{}, rb.lattice[1].blocks[0].Hash: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) s.Require().NotNil(rb.lattice[0].blocks[2]) // Add block 0-3 which acks 0-2. h = rb.lattice[0].blocks[2].Hash b = &types.Block{ ProposerID: vids[0], ParentHash: h, Hash: common.NewRandomHash(), Timestamps: genTimestamps(vids), Position: types.Position{ ChainID: 0, Height: 3, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) s.Require().NotNil(rb.lattice[0].blocks[3]) // Add block 3-1 which acks 3-0. h = rb.lattice[3].blocks[0].Hash b = &types.Block{ ProposerID: vids[3], ParentHash: h, Hash: common.NewRandomHash(), Timestamps: genTimestamps(vids), Position: types.Position{ ChainID: 3, Height: 1, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) s.Require().NotNil(rb.lattice[3].blocks[0]) return vids } func (s *ReliableBroadcastTest) TestAddValidator() { rb := newReliableBroadcast() s.Require().Equal(len(rb.lattice), 0) vids := genTestCase1(s, rb) s.Require().Equal(len(rb.lattice), 4) for _, vid := range vids { rb.deleteValidator(vid) } } func (s *ReliableBroadcastTest) TestSanityCheck() { var b *types.Block var h common.Hash var vids []types.ValidatorID var err error rb := newReliableBroadcast() vids = genTestCase1(s, rb) // Non-genesis block with no ack, should get error. b = &types.Block{ ProposerID: vids[0], ParentHash: common.NewRandomHash(), Position: types.Position{ ChainID: 0, Height: 10, }, Acks: make(map[common.Hash]struct{}), } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrNotAckParent.Error(), err.Error()) // Non-genesis block which does not ack its parent. b = &types.Block{ ProposerID: vids[1], ParentHash: common.NewRandomHash(), Position: types.Position{ ChainID: 1, Height: 1, }, Acks: map[common.Hash]struct{}{ rb.lattice[2].blocks[0].Hash: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrNotAckParent.Error(), err.Error()) // Non-genesis block which acks its parent but the height is invalid. h = rb.lattice[1].blocks[0].Hash b = &types.Block{ ProposerID: vids[1], ParentHash: h, Position: types.Position{ ChainID: 1, Height: 2, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrInvalidBlockHeight.Error(), err.Error()) // Invalid proposer ID. h = rb.lattice[1].blocks[0].Hash b = &types.Block{ ProposerID: types.ValidatorID{Hash: common.NewRandomHash()}, ParentHash: h, Position: types.Position{ Height: 1, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrInvalidProposerID.Error(), err.Error()) // Invalid chain ID. h = rb.lattice[1].blocks[0].Hash b = &types.Block{ ProposerID: vids[1], ParentHash: h, Position: types.Position{ ChainID: 100, Height: 1, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrInvalidChainID.Error(), err.Error()) // Fork block. h = rb.lattice[0].blocks[0].Hash b = &types.Block{ ProposerID: vids[0], ParentHash: h, Position: types.Position{ ChainID: 0, Height: 1, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, }, Timestamps: map[types.ValidatorID]time.Time{ vids[0]: time.Now().UTC(), }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrForkBlock.Error(), err.Error()) // Replicated ack. h = rb.lattice[0].blocks[3].Hash b = &types.Block{ ProposerID: vids[0], ParentHash: h, Position: types.Position{ ChainID: 0, Height: 4, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, rb.lattice[1].blocks[0].Hash: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Require().NotNil(err) s.Require().Equal(ErrDoubleAck.Error(), err.Error()) // Normal block. h = rb.lattice[1].blocks[0].Hash b = &types.Block{ ProposerID: vids[1], ParentHash: h, Position: types.Position{ ChainID: 1, Height: 1, }, Acks: map[common.Hash]struct{}{ h: struct{}{}, common.NewRandomHash(): struct{}{}, }, Timestamps: genTimestamps(vids), } b.Hash, err = hashBlock(b) s.Require().Nil(err) err = rb.sanityCheck(b) s.Nil(err) } func (s *ReliableBroadcastTest) TestAreAllAcksInLattice() { var b *types.Block rb := newReliableBroadcast() genTestCase1(s, rb) // Empty ack should get true, although won't pass sanity check. b = &types.Block{ Acks: map[common.Hash]struct{}{}, } s.Require().True(rb.areAllAcksInLattice(b)) // Acks blocks in lattice b = &types.Block{ Acks: map[common.Hash]struct{}{ rb.lattice[0].blocks[0].Hash: struct{}{}, rb.lattice[0].blocks[1].Hash: struct{}{}, }, } s.Require().True(rb.areAllAcksInLattice(b)) // Acks random block hash. b = &types.Block{ Acks: map[common.Hash]struct{}{ common.NewRandomHash(): struct{}{}, }, } s.Require().False(rb.areAllAcksInLattice(b)) } func (s *ReliableBroadcastTest) TestStrongAck() { var b *types.Block var vids []types.ValidatorID rb := newReliableBroadcast() vids = genTestCase1(s, rb) // Check block 0-0 to 0-3 before adding 1-1 and 2-1. for i := uint64(0); i < 4; i++ { s.Require().Equal(blockStatusInit, rb.blockInfos[rb.lattice[0].blocks[i].Hash].status) } // Add block 1-1 which acks 1-0 and 0-2, and block 0-0 to 0-3 are still // in blockStatusInit, because they are not strongly acked. b = &types.Block{ ProposerID: vids[1], ParentHash: rb.lattice[1].blocks[0].Hash, Hash: common.NewRandomHash(), Position: types.Position{ ChainID: 1, Height: 1, }, Timestamps: genTimestamps(vids), Acks: map[common.Hash]struct{}{ rb.lattice[0].blocks[2].Hash: struct{}{}, rb.lattice[1].blocks[0].Hash: struct{}{}, }, } var err error b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) s.Require().NotNil(rb.lattice[1].blocks[1]) for i := uint64(0); i < 4; i++ { h := rb.lattice[0].blocks[i].Hash s.Require().Equal(blockStatusInit, rb.blockInfos[h].status) } // Add block 2-1 which acks 0-2 and 2-0, block 0-0 to 0-2 are strongly acked but // 0-3 is still not. b = &types.Block{ ProposerID: vids[2], ParentHash: rb.lattice[2].blocks[0].Hash, Hash: common.NewRandomHash(), Position: types.Position{ ChainID: 2, Height: 1, }, Timestamps: genTimestamps(vids), Acks: map[common.Hash]struct{}{ rb.lattice[0].blocks[2].Hash: struct{}{}, rb.lattice[2].blocks[0].Hash: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) s.Require().Equal(blockStatusAcked, rb.blockInfos[rb.lattice[0].blocks[0].Hash].status) s.Require().Equal(blockStatusAcked, rb.blockInfos[rb.lattice[0].blocks[1].Hash].status) s.Require().Equal(blockStatusAcked, rb.blockInfos[rb.lattice[0].blocks[2].Hash].status) s.Require().Equal(blockStatusInit, rb.blockInfos[rb.lattice[0].blocks[3].Hash].status) } func (s *ReliableBroadcastTest) TestExtractBlocks() { var b *types.Block rb := newReliableBroadcast() vids := genTestCase1(s, rb) // Add block 1-1 which acks 1-0, 0-2, 3-0. b = &types.Block{ ProposerID: vids[1], ParentHash: rb.lattice[1].blocks[0].Hash, Hash: common.NewRandomHash(), Position: types.Position{ ChainID: 1, Height: 1, }, Timestamps: genTimestamps(vids), Acks: map[common.Hash]struct{}{ rb.lattice[0].blocks[2].Hash: struct{}{}, rb.lattice[1].blocks[0].Hash: struct{}{}, rb.lattice[3].blocks[0].Hash: struct{}{}, }, } var err error b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) // Add block 2-1 which acks 0-2, 2-0, 3-0. b = &types.Block{ ProposerID: vids[2], ParentHash: rb.lattice[2].blocks[0].Hash, Hash: common.NewRandomHash(), Position: types.Position{ ChainID: 2, Height: 1, }, Timestamps: genTimestamps(vids), Acks: map[common.Hash]struct{}{ rb.lattice[0].blocks[2].Hash: struct{}{}, rb.lattice[2].blocks[0].Hash: struct{}{}, rb.lattice[3].blocks[0].Hash: struct{}{}, }, } b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) hashes := []common.Hash{ rb.lattice[0].blocks[0].Hash, rb.lattice[0].blocks[1].Hash, rb.lattice[3].blocks[0].Hash, } hashExtracted := map[common.Hash]*types.Block{} for _, b := range rb.extractBlocks() { hashExtracted[b.Hash] = b s.Require().Equal(blockStatusOrdering, rb.blockInfos[b.Hash].status) } for _, h := range hashes { _, exist := hashExtracted[h] s.Require().True(exist) } } func (s *ReliableBroadcastTest) TestRandomIntensiveAcking() { rb := newReliableBroadcast() vids := test.GenerateRandomValidatorIDs(4) heights := map[types.ValidatorID]uint64{} extractedBlocks := []*types.Block{} // Generate validators. for _, vid := range vids { rb.addValidator(vid) } rb.setChainNum(uint32(len(vids))) // Generate genesis blocks. for _, vid := range vids { b := s.prepareGenesisBlock(vid, vids) s.Require().Nil(rb.processBlock(b)) heights[vid] = 1 } for i := 0; i < 5000; i++ { id := rand.Int() % len(vids) vid := vids[id] height := heights[vid] heights[vid]++ parentHash := rb.lattice[id].blocks[height-1].Hash acks := map[common.Hash]struct{}{} for id2 := range vids { if b, exist := rb.lattice[id2].blocks[rb.lattice[id].nextAck[id2]]; exist { acks[b.Hash] = struct{}{} } } b := &types.Block{ ProposerID: vid, ParentHash: parentHash, Position: types.Position{ ChainID: uint32(id), Height: height, }, Timestamps: genTimestamps(vids), Acks: acks, } var err error b.Hash, err = hashBlock(b) s.Require().Nil(err) s.Require().Nil(rb.processBlock(b)) extractedBlocks = append(extractedBlocks, rb.extractBlocks()...) } extractedBlocks = append(extractedBlocks, rb.extractBlocks()...) // The len of array extractedBlocks should be about 5000. s.Require().True(len(extractedBlocks) > 4500) // The len of rb.blockInfos should be small if deleting mechanism works. // s.True(len(rb.blockInfos) < 500) } func (s *ReliableBroadcastTest) TestRandomlyGeneratedBlocks() { var ( validatorCount = 19 blockCount = 50 repeat = 20 ) // Prepare a randomly generated blocks. db, err := blockdb.NewMemBackedBlockDB("test-reliable-broadcast-random.blockdb") s.Require().Nil(err) defer func() { // If the test fails, keep the block database for troubleshooting. if s.T().Failed() { s.Nil(db.Close()) } }() gen := test.NewBlocksGenerator(nil, hashBlock) _, err = gen.Generate(validatorCount, blockCount, nil, db) s.Require().Nil(err) iter, err := db.GetAll() s.Require().Nil(err) // Setup a revealer that would reveal blocks randomly. revealer, err := test.NewRandomRevealer(iter) s.Require().Nil(err) stronglyAckedHashesAsString := map[string]struct{}{} for i := 0; i < repeat; i++ { validators := map[types.ValidatorID]struct{}{} rb := newReliableBroadcast() rb.setChainNum(uint32(validatorCount)) stronglyAckedHashes := common.Hashes{} revealer.Reset() for { // Reveal next block. b, err := revealer.Next() if err != nil { if err == blockdb.ErrIterationFinished { err = nil break } } s.Require().Nil(err) // It's a hack to add validator to reliableBroadcast module. if _, added := validators[b.ProposerID]; !added { rb.addValidator(b.ProposerID) validators[b.ProposerID] = struct{}{} } // Perform reliable broadcast process. s.Require().Nil(rb.processBlock(&b)) for _, b := range rb.extractBlocks() { stronglyAckedHashes = append(stronglyAckedHashes, b.Hash) } } // To make it easier to check, sort hashes of // strongly acked blocks, and concatenate them into // a string. sort.Sort(stronglyAckedHashes) asString := "" for _, h := range stronglyAckedHashes { asString += h.String() + "," } stronglyAckedHashesAsString[asString] = struct{}{} } // Make sure concatenated hashes of strongly acked blocks are identical. s.Require().Len(stronglyAckedHashesAsString, 1) for h := range stronglyAckedHashesAsString { // Make sure at least some blocks are strongly acked. s.True(len(h) > 0) } } func (s *ReliableBroadcastTest) TestPrepareBlock() { var ( req = s.Require() rb = newReliableBroadcast() minInterval = 50 * time.Millisecond validators = test.GenerateRandomValidatorIDs(4) ) // Prepare validator IDs. for _, vID := range validators { rb.addValidator(vID) } rb.setChainNum(uint32(len(validators))) // Setup genesis blocks. b00 := s.prepareGenesisBlock(validators[0], validators) time.Sleep(minInterval) b10 := s.prepareGenesisBlock(validators[1], validators) time.Sleep(minInterval) b20 := s.prepareGenesisBlock(validators[2], validators) time.Sleep(minInterval) b30 := s.prepareGenesisBlock(validators[3], validators) // Submit these blocks to reliableBroadcast instance. s.Require().Nil(rb.processBlock(b00)) s.Require().Nil(rb.processBlock(b10)) s.Require().Nil(rb.processBlock(b20)) s.Require().Nil(rb.processBlock(b30)) // We should be able to collect all 4 genesis blocks by calling // prepareBlock. b11 := &types.Block{ ProposerID: validators[1], Position: types.Position{ ChainID: 1, }, } rb.prepareBlock(b11) var err error b11.Hash, err = hashBlock(b11) s.Require().Nil(err) req.Contains(b11.Acks, b00.Hash) req.Contains(b11.Acks, b10.Hash) req.Contains(b11.Acks, b20.Hash) req.Contains(b11.Acks, b30.Hash) req.Equal(b11.Timestamps[validators[0]], b00.Timestamps[b00.ProposerID].Add(time.Millisecond)) req.Equal(b11.Timestamps[validators[1]], b10.Timestamps[b10.ProposerID].Add(time.Millisecond)) req.Equal(b11.Timestamps[validators[2]], b20.Timestamps[b20.ProposerID].Add(time.Millisecond)) req.Equal(b11.Timestamps[validators[3]], b30.Timestamps[b30.ProposerID].Add(time.Millisecond)) req.Equal(b11.ParentHash, b10.Hash) req.Equal(b11.Position.Height, uint64(1)) s.Require().Nil(rb.processBlock(b11)) // Propose/Process a block based on collected info. b12 := &types.Block{ ProposerID: validators[1], Position: types.Position{ ChainID: 1, }, } rb.prepareBlock(b12) b12.Hash, err = hashBlock(b12) s.Require().Nil(err) // This time we only need to ack b11. req.Len(b12.Acks, 1) req.Contains(b12.Acks, b11.Hash) req.Equal(b12.ParentHash, b11.Hash) req.Equal(b12.Position.Height, uint64(2)) // When calling with other validator ID, we should be able to // get 4 blocks to ack. b01 := &types.Block{ ProposerID: validators[0], Position: types.Position{ ChainID: 0, }, } rb.prepareBlock(b01) b01.Hash, err = hashBlock(b01) s.Require().Nil(err) req.Len(b01.Acks, 4) req.Contains(b01.Acks, b00.Hash) req.Contains(b01.Acks, b11.Hash) req.Contains(b01.Acks, b20.Hash) req.Contains(b01.Acks, b30.Hash) req.Equal(b01.ParentHash, b00.Hash) req.Equal(b01.Position.Height, uint64(1)) } func TestReliableBroadcast(t *testing.T) { suite.Run(t, new(ReliableBroadcastTest)) }