// Copyright 2019 The dexon-consensus Authors // This file is part of the dexon-consensus library. // // The dexon-consensus 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 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 library. If not, see // . package core import ( "fmt" "math/rand" "testing" "time" "github.com/dexon-foundation/dexon-consensus/common" "github.com/dexon-foundation/dexon-consensus/core/crypto" "github.com/dexon-foundation/dexon-consensus/core/test" "github.com/dexon-foundation/dexon-consensus/core/types" "github.com/dexon-foundation/dexon-consensus/core/utils" "github.com/stretchr/testify/suite" ) type testTSigVerifier struct{} func (v *testTSigVerifier) VerifySignature(hash common.Hash, sig crypto.Signature) bool { return true } type testTSigVerifierGetter struct{} func (t *testTSigVerifierGetter) UpdateAndGet(round uint64) ( TSigVerifier, bool, error) { return &testTSigVerifier{}, true, nil } func (t *testTSigVerifierGetter) Purge(_ uint64) {} type BlockChainTestSuite struct { suite.Suite nID types.NodeID signer *utils.Signer dMoment time.Time blockInterval time.Duration } func (s *BlockChainTestSuite) SetupSuite() { prvKeys, pubKeys, err := test.NewKeys(1) s.Require().NoError(err) s.nID = types.NewNodeID(pubKeys[0]) s.signer = utils.NewSigner(prvKeys[0]) s.dMoment = time.Now().UTC() s.blockInterval = 1 * time.Millisecond } func (s *BlockChainTestSuite) newBlocks(c uint64, initBlock *types.Block) ( blocks []*types.Block) { parentHash := common.Hash{} baseHeight := types.GenesisHeight t := s.dMoment.Add(s.blockInterval) initRound := uint64(0) if initBlock != nil { parentHash = initBlock.Hash t = initBlock.Timestamp.Add(s.blockInterval) initRound = initBlock.Position.Round baseHeight = initBlock.Position.Height + 1 } for i := uint64(0); i < uint64(c); i++ { b := &types.Block{ ParentHash: parentHash, Position: types.Position{Round: initRound, Height: baseHeight + i}, Timestamp: t, } if b.Position.Round >= DKGDelayRound { b.Randomness = common.GenerateRandomBytes() } else { b.Randomness = NoRand } s.Require().NoError(s.signer.SignBlock(b)) blocks = append(blocks, b) parentHash = b.Hash t = t.Add(s.blockInterval) } return } func (s *BlockChainTestSuite) newEmptyBlock(parent *types.Block, blockInterval time.Duration) *types.Block { emptyB := &types.Block{ ParentHash: parent.Hash, Position: types.Position{ Round: parent.Position.Round, Height: parent.Position.Height + 1, }, Timestamp: parent.Timestamp.Add(blockInterval), } var err error emptyB.Hash, err = utils.HashBlock(emptyB) s.Require().NoError(err) return emptyB } func (s *BlockChainTestSuite) newBlock(parent *types.Block, round uint64, blockInterval time.Duration) *types.Block { b := &types.Block{ ParentHash: parent.Hash, Position: types.Position{ Round: round, Height: parent.Position.Height + 1, }, Timestamp: parent.Timestamp.Add(blockInterval), } if b.Position.Round >= DKGDelayRound { b.Randomness = common.GenerateRandomBytes() } else { b.Randomness = NoRand } s.Require().NoError(s.signer.SignBlock(b)) return b } func (s *BlockChainTestSuite) newRandomnessFromBlock( b *types.Block) *types.AgreementResult { return &types.AgreementResult{ BlockHash: b.Hash, Position: b.Position, Randomness: common.GenerateRandomBytes(), } } func (s *BlockChainTestSuite) newBlockChain(initB *types.Block, roundLength uint64) (bc *blockChain) { initRound := uint64(0) if initB != nil { initRound = initB.Position.Round } initHeight := types.GenesisHeight if initB != nil { initHeight = initB.Position.Height } bc = newBlockChain(s.nID, s.dMoment, initB, test.NewApp(0, nil, nil), &testTSigVerifierGetter{}, s.signer, &common.NullLogger{}) // Provide the genesis round event. s.Require().NoError(bc.notifyRoundEvents([]utils.RoundEventParam{ utils.RoundEventParam{ Round: initRound, Reset: 0, BeginHeight: initHeight, Config: &types.Config{ MinBlockInterval: s.blockInterval, RoundLength: roundLength, }}})) return } func (s *BlockChainTestSuite) newRoundOneInitBlock() *types.Block { initBlock := &types.Block{ ParentHash: common.NewRandomHash(), Position: types.Position{Round: 1}, Timestamp: s.dMoment, } s.Require().NoError(s.signer.SignBlock(initBlock)) return initBlock } func (s *BlockChainTestSuite) baseConcurrentAceessTest(initBlock *types.Block, blocks []*types.Block, results []*types.AgreementResult) { var ( bc = s.newBlockChain(initBlock, uint64(len(blocks)+1)) start = make(chan struct{}) newNotif = make(chan struct{}, 1) delivered []*types.Block ) resultsCopy := make([]*types.AgreementResult, len(results)) copy(resultsCopy, results) type randomnessResult types.AgreementResult add := func(v interface{}) { <-start switch val := v.(type) { case *types.Block: if err := bc.addBlock(val); err != nil { // Never assertion in sub routine when testing. panic(err) } case *types.AgreementResult: if err := bc.processAgreementResult(val); err != nil { if err != ErrSkipButNoError { // Never assertion in sub routine when testing. panic(err) } } case *randomnessResult: bc.addBlockRandomness(val.Position, val.Randomness) default: panic(fmt.Errorf("unknown type: %v", v)) } select { case newNotif <- struct{}{}: default: } } rand.Shuffle(len(resultsCopy), func(i, j int) { resultsCopy[i], resultsCopy[j] = resultsCopy[j], resultsCopy[i] }) for _, b := range blocks { go add(b) } for i, r := range resultsCopy { if i >= len(resultsCopy)/2 { break } go add((*randomnessResult)(r)) } go func() { for i, a := range resultsCopy { if i < len(resultsCopy)/2 { continue } add(a) } }() close(start) for { select { case <-newNotif: delivered = append(delivered, bc.extractBlocks()...) case <-time.After(100 * time.Millisecond): delivered = append(delivered, bc.extractBlocks()...) } if len(delivered) == len(blocks) { break } } // Check result. b := delivered[0] s.Require().Equal(b.Position.Height, uint64(1)) s.Require().NotEmpty(b.Randomness) for _, bb := range delivered[1:] { s.Require().Equal(b.Position.Height+1, bb.Position.Height) s.Require().NotEmpty(b.Randomness) b = bb } } func (s *BlockChainTestSuite) TestBasicUsage() { initBlock := s.newRoundOneInitBlock() bc := s.newBlockChain(initBlock, 10) // test scenario: block, empty block, randomness can be added in any order // of position. blocks := s.newBlocks(4, initBlock) b0, b1, b2, b3 := blocks[0], blocks[1], blocks[2], blocks[3] // generate block-5 after block-4, which is an empty block. b4 := s.newEmptyBlock(b3, time.Millisecond) b5 := &types.Block{ ParentHash: b4.Hash, Position: types.Position{Round: 1, Height: b4.Position.Height + 1}, Randomness: common.GenerateRandomBytes(), } s.Require().NoError(s.signer.SignBlock(b5)) s.Require().NoError(bc.addBlock(b5)) emptyB, err := bc.addEmptyBlock(b4.Position) s.Require().Nil(emptyB) s.Require().NoError(err) s.Require().NoError(bc.addBlock(b3)) s.Require().NoError(bc.addBlock(b2)) s.Require().NoError(bc.addBlock(b1)) s.Require().NoError(bc.addBlock(b0)) extracted := bc.extractBlocks() s.Require().Len(extracted, 4) bc.pendingRandomnesses[b4.Position] = common.GenerateRandomBytes() extracted = bc.extractBlocks() s.Require().Len(extracted, 2) s.Require().Equal(extracted[0].Hash, b4.Hash) extracted = bc.extractBlocks() s.Require().Len(extracted, 0) } func (s *BlockChainTestSuite) TestConcurrentAccess() { // Raise one go routine for each block and randomness. And let them try to // add to blockChain at the same time. Make sure we can delivered them all. var ( retry = 10 initBlock = s.newRoundOneInitBlock() blocks = s.newBlocks(500, initBlock) rands = []*types.AgreementResult{} ) for _, b := range blocks { rands = append(rands, s.newRandomnessFromBlock(b)) } for i := 0; i < retry; i++ { s.baseConcurrentAceessTest(initBlock, blocks, rands) } } func (s *BlockChainTestSuite) TestSanityCheck() { bc := s.newBlockChain(nil, 4) blocks := s.newBlocks(3, nil) b0, b1, b2 := blocks[0], blocks[1], blocks[2] // ErrNotGenesisBlock s.Require().Equal(ErrNotGenesisBlock.Error(), bc.sanityCheck(b1).Error()) // Genesis block should pass sanity check. s.Require().NoError(bc.sanityCheck(b0)) s.Require().NoError(bc.addBlock(b0)) // ErrIsGenesisBlock s.Require().Equal(ErrIsGenesisBlock.Error(), bc.sanityCheck(b0).Error()) // ErrRetrySanityCheckLater s.Require().Equal( ErrRetrySanityCheckLater.Error(), bc.sanityCheck(b2).Error()) // ErrInvalidBlockHeight s.Require().NoError(bc.addBlock(b1)) s.Require().NoError(bc.addBlock(b2)) s.Require().Equal( ErrInvalidBlockHeight.Error(), bc.sanityCheck(b1).Error()) // ErrInvalidRoundID // Should not switch round when tip is not the last block. s.Require().Equal( ErrInvalidRoundID.Error(), bc.sanityCheck(s.newBlock(b2, 1, 1*time.Second)).Error()) b3 := s.newBlock(b2, 0, 100*time.Second) s.Require().NoError(bc.addBlock(b3)) // Should switch round when tip is the last block. s.Require().Equal( ErrRoundNotSwitch.Error(), bc.sanityCheck(s.newBlock(b3, 0, 1*time.Second)).Error()) b4 := &types.Block{ ParentHash: b2.Hash, Position: types.Position{ Round: 1, Height: 5, }, Timestamp: b3.Timestamp, } s.Require().NoError(s.signer.SignBlock(b4)) // ErrIncorrectParentHash s.Require().EqualError(ErrIncorrectParentHash, bc.sanityCheck(b4).Error()) b4.ParentHash = b3.Hash // ErrInvalidTimestamp s.Require().EqualError(ErrInvalidTimestamp, bc.sanityCheck(b4).Error()) b4.Timestamp = b3.Timestamp.Add(1 * time.Second) // There is no valid signature attached. s.Require().Error(bc.sanityCheck(b4)) // OK case. s.Require().NoError(s.signer.SignBlock(b4)) s.Require().NoError(bc.sanityCheck(b4)) } func (s *BlockChainTestSuite) TestNotifyRoundEvents() { roundLength := uint64(10) bc := s.newBlockChain(nil, roundLength) newEvent := func(round, reset, height uint64) []utils.RoundEventParam { return []utils.RoundEventParam{ utils.RoundEventParam{ Round: round, Reset: reset, BeginHeight: types.GenesisHeight + height, CRS: common.Hash{}, Config: &types.Config{RoundLength: roundLength}, }} } s.Require().Equal(ErrInvalidRoundID.Error(), bc.notifyRoundEvents(newEvent(2, 0, roundLength)).Error()) s.Require().NoError(bc.notifyRoundEvents(newEvent(1, 0, roundLength))) // Make sure new config is appended when new round is ready. s.Require().Len(bc.configs, 2) s.Require().Equal(ErrInvalidRoundID.Error(), bc.notifyRoundEvents(newEvent(3, 1, roundLength*2)).Error()) s.Require().Equal(ErrInvalidBlockHeight.Error(), bc.notifyRoundEvents(newEvent(1, 1, roundLength)).Error()) s.Require().NoError(bc.notifyRoundEvents(newEvent(1, 1, roundLength*2))) // Make sure roundEndHeight is extended when DKG reset. s.Require().Equal(bc.configs[len(bc.configs)-1].RoundEndHeight(), types.GenesisHeight+roundLength*3) } func (s *BlockChainTestSuite) TestConfirmed() { bc := s.newBlockChain(nil, 10) blocks := s.newBlocks(3, nil) // Add a confirmed block. s.Require().NoError(bc.addBlock(blocks[0])) // Add a pending block. s.Require().NoError(bc.addBlock(blocks[2])) s.Require().True(bc.confirmed(1)) s.Require().False(bc.confirmed(2)) s.Require().True(bc.confirmed(3)) } func (s *BlockChainTestSuite) TestNextBlockAndTipRound() { var roundLength uint64 = 3 bc := s.newBlockChain(nil, roundLength) s.Require().NoError(bc.notifyRoundEvents([]utils.RoundEventParam{ utils.RoundEventParam{ Round: 1, Reset: 0, BeginHeight: types.GenesisHeight + roundLength, CRS: common.Hash{}, Config: &types.Config{ MinBlockInterval: s.blockInterval, RoundLength: roundLength, }}})) blocks := s.newBlocks(3, nil) nextH, nextT := bc.nextBlock() s.Require().Equal(nextH, types.GenesisHeight) s.Require().Equal(nextT, s.dMoment) // Add one block. s.Require().NoError(bc.addBlock(blocks[0])) s.Require().Len(bc.extractBlocks(), 1) nextH, nextT = bc.nextBlock() s.Require().Equal(nextH, uint64(2)) s.Require().Equal( nextT, blocks[0].Timestamp.Add(bc.configs[0].minBlockInterval)) // Add one block, expected to be pending. s.Require().NoError(bc.addBlock(blocks[2])) nextH2, nextT2 := bc.nextBlock() s.Require().Equal(nextH, nextH2) s.Require().Equal(nextT, nextT2) // Add a block, which is the last block of this round. b3 := s.newBlock(blocks[2], 1, 1*time.Second) s.Require().NoError(bc.addBlock(blocks[1])) s.Require().NoError(bc.sanityCheck(b3)) s.Require().NoError(bc.addBlock(b3)) s.Require().Equal(bc.tipRound(), uint64(1)) } func (s *BlockChainTestSuite) TestPendingBlocksWithoutRandomness() { initBlock := s.newRoundOneInitBlock() bc := s.newBlockChain(initBlock, 10) b0, err := bc.addEmptyBlock(types.Position{Round: 1, Height: 1}) s.Require().NoError(err) b1, err := bc.addEmptyBlock(types.Position{Round: 1, Height: 2}) s.Require().NoError(err) b2, err := bc.addEmptyBlock(types.Position{Round: 1, Height: 3}) s.Require().NoError(err) s.Require().Equal(bc.pendingBlocksWithoutRandomness(), []*types.Block{ b0, b1, b2}) s.Require().NoError(bc.processAgreementResult(s.newRandomnessFromBlock(b0))) s.Require().Equal(bc.pendingBlocksWithoutRandomness(), []*types.Block{ b1, b2}) } func (s *BlockChainTestSuite) TestLastXBlock() { initBlock := s.newRoundOneInitBlock() bc := s.newBlockChain(initBlock, 10) s.Require().Nil(bc.lastPendingBlock()) s.Require().True(bc.lastDeliveredBlock() == initBlock) blocks := s.newBlocks(2, initBlock) s.Require().NoError(bc.addBlock(blocks[0])) s.Require().True(bc.lastPendingBlock() == blocks[0]) s.Require().True(bc.lastDeliveredBlock() == initBlock) s.Require().Len(bc.extractBlocks(), 1) s.Require().Nil(bc.lastPendingBlock()) s.Require().True(bc.lastDeliveredBlock() == blocks[0]) s.Require().NoError(bc.addBlock(blocks[1])) s.Require().True(bc.lastPendingBlock() == blocks[1]) s.Require().True(bc.lastDeliveredBlock() == blocks[0]) } func (s *BlockChainTestSuite) TestPendingBlockRecords() { bs := s.newBlocks(5, nil) ps := pendingBlockRecords{} s.Require().NoError(ps.insert(pendingBlockRecord{bs[2].Position, bs[2]})) s.Require().NoError(ps.insert(pendingBlockRecord{bs[1].Position, bs[1]})) s.Require().NoError(ps.insert(pendingBlockRecord{bs[0].Position, bs[0]})) s.Require().Equal(ErrDuplicatedPendingBlock.Error(), ps.insert(pendingBlockRecord{bs[0].Position, nil}).Error()) s.Require().True(ps[0].position.Equal(bs[0].Position)) s.Require().True(ps[1].position.Equal(bs[1].Position)) s.Require().True(ps[2].position.Equal(bs[2].Position)) s.Require().NoError(ps.insert(pendingBlockRecord{bs[4].Position, bs[4]})) // Here assume block3 is empty, since we didn't verify parent hash in // pendingBlockRecords, it should be fine. s.Require().NoError(ps.insert(pendingBlockRecord{bs[3].Position, nil})) s.Require().True(ps[3].position.Equal(bs[3].Position)) s.Require().True(ps[4].position.Equal(bs[4].Position)) } func (s *BlockChainTestSuite) TestFindPendingBlock() { bc := s.newBlockChain(nil, 10) blocks := s.newBlocks(7, nil) s.Require().NoError(bc.addBlock(blocks[6])) s.Require().NoError(bc.addBlock(blocks[5])) s.Require().NoError(bc.addBlock(blocks[3])) s.Require().NoError(bc.addBlock(blocks[2])) s.Require().NoError(bc.addBlock(blocks[1])) s.Require().NoError(bc.addBlock(blocks[0])) s.Require().True(bc.findPendingBlock(blocks[0].Position) == blocks[0]) s.Require().True(bc.findPendingBlock(blocks[1].Position) == blocks[1]) s.Require().True(bc.findPendingBlock(blocks[2].Position) == blocks[2]) s.Require().True(bc.findPendingBlock(blocks[3].Position) == blocks[3]) s.Require().Nil(bc.findPendingBlock(blocks[4].Position)) s.Require().True(bc.findPendingBlock(blocks[5].Position) == blocks[5]) s.Require().True(bc.findPendingBlock(blocks[6].Position) == blocks[6]) } func (s *BlockChainTestSuite) TestAddEmptyBlockDirectly() { bc := s.newBlockChain(nil, 10) blocks := s.newBlocks(1, nil) s.Require().NoError(bc.addBlock(blocks[0])) // Add an empty block after a normal block. pos := types.Position{Height: 2} emptyB1, err := bc.addEmptyBlock(pos) s.Require().NotNil(emptyB1) s.Require().True(emptyB1.Position.Equal(pos)) s.Require().NoError(err) // Add an empty block after an empty block. pos = types.Position{Height: 3} emptyB2, err := bc.addEmptyBlock(pos) s.Require().NotNil(emptyB2) s.Require().True(emptyB2.Position.Equal(pos)) s.Require().NoError(err) // prepare a normal block. pos = types.Position{Height: 4} expectedTimestamp := emptyB2.Timestamp.Add(s.blockInterval) b3, err := bc.proposeBlock(pos, expectedTimestamp.Add(-100*time.Second), false) s.Require().NotNil(b3) s.Require().NoError(err) // The timestamp should be refined. s.Require().True(b3.Timestamp.Equal(expectedTimestamp)) // Add an empty block far away from current tip. pos = types.Position{Height: 5} emptyB4, err := bc.addEmptyBlock(pos) s.Require().Nil(emptyB4) s.Require().NoError(err) // propose an empty block based on the block at height=3, which mimics the // scenario that the empty block is pulled from others. emptyB4 = &types.Block{ ParentHash: b3.Hash, Position: pos, Timestamp: b3.Timestamp.Add(s.blockInterval), Witness: types.Witness{ Height: b3.Witness.Height, Data: b3.Witness.Data, // Hacky, don't worry. }, } emptyB4.Hash, err = utils.HashBlock(emptyB4) s.Require().NoError(err) s.Require().NoError(bc.addBlock(emptyB4)) rec, found := bc.pendingBlocks.searchByHeight(5) s.Require().True(found) s.Require().NotNil(rec.block) } func (s *BlockChainTestSuite) TestPrepareBlock() { roundLength := uint64(2) bc := s.newBlockChain(nil, roundLength) // Try to propose blocks at height=0. b0, err := bc.prepareBlock(types.Position{Height: types.GenesisHeight + 1}, s.dMoment, false) s.Require().Nil(b0) s.Require().EqualError(ErrNotGenesisBlock, err.Error()) b0, err = bc.prepareBlock(types.Position{Height: types.GenesisHeight}, s.dMoment, false) s.Require().NoError(err) s.Require().Equal(b0.Position, types.Position{Height: types.GenesisHeight}) s.Require().True(b0.Timestamp.Equal(s.dMoment.Add(s.blockInterval))) empty0, err := bc.prepareBlock(types.Position{Height: types.GenesisHeight}, s.dMoment, true) s.Require().NoError(err) s.Require().Equal(empty0.Position, types.Position{ Height: types.GenesisHeight}) s.Require().True(empty0.Timestamp.Equal(s.dMoment.Add(s.blockInterval))) // Try to propose blocks at height=1. s.Require().NoError(bc.addBlock(b0)) prepare1 := func(empty bool) *types.Block { b, err := bc.prepareBlock(types.Position{Height: types.GenesisHeight}, s.dMoment, empty) s.Require().Nil(b) s.Require().EqualError(ErrNotFollowTipPosition, err.Error()) b, err = bc.prepareBlock(types.Position{ Height: types.GenesisHeight + 2}, s.dMoment, empty) s.Require().Nil(b) s.Require().EqualError(ErrNotFollowTipPosition, err.Error()) b, err = bc.prepareBlock(types.Position{ Round: 1, Height: types.GenesisHeight + 1}, s.dMoment, empty) s.Require().Nil(b) s.Require().EqualError(ErrInvalidRoundID, err.Error()) b, err = bc.prepareBlock(types.Position{ Height: types.GenesisHeight + 1}, s.dMoment, empty) s.Require().NoError(err) s.Require().NotNil(b) s.Require().Equal(b.ParentHash, b0.Hash) s.Require().True(b.Timestamp.Equal(b0.Timestamp.Add(s.blockInterval))) return b } b1 := prepare1(false) prepare1(true) // Try to propose blocks at height=2, which should trigger round switch. s.Require().NoError(bc.notifyRoundEvents([]utils.RoundEventParam{ utils.RoundEventParam{ Round: 1, Reset: 0, BeginHeight: types.GenesisHeight + roundLength, Config: &types.Config{ MinBlockInterval: s.blockInterval, RoundLength: roundLength, }}})) s.Require().NoError(bc.addBlock(b1)) prepare2 := func(empty bool) *types.Block { b, err := bc.prepareBlock(types.Position{ Height: types.GenesisHeight + 2}, s.dMoment, empty) s.Require().EqualError(ErrRoundNotSwitch, err.Error()) s.Require().Nil(b) b, err = bc.prepareBlock(types.Position{ Round: 1, Height: types.GenesisHeight + 2}, s.dMoment, empty) s.Require().NoError(err) s.Require().NotNil(b) s.Require().Equal(b.ParentHash, b1.Hash) s.Require().True(b.Timestamp.Equal(b1.Timestamp.Add(s.blockInterval))) return b } prepare2(false) prepare2(true) } func TestBlockChain(t *testing.T) { suite.Run(t, new(BlockChainTestSuite)) }