aboutsummaryrefslogtreecommitdiffstats
path: root/core/blockchain_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'core/blockchain_test.go')
-rw-r--r--core/blockchain_test.go462
1 files changed, 462 insertions, 0 deletions
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
new file mode 100644
index 0000000..908b04f
--- /dev/null
+++ b/core/blockchain_test.go
@@ -0,0 +1,462 @@
+// 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
+// <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "fmt"
+ "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
+}
+
+type BlockChainTestSuite struct {
+ suite.Suite
+
+ nID types.NodeID
+ signer *utils.Signer
+ dMoment time.Time
+ blockInterval time.Duration
+ roundInterval 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
+ s.roundInterval = 10 * time.Second
+}
+
+func (s *BlockChainTestSuite) newBlocks(c uint64, initBlock *types.Block) (
+ blocks []*types.Block) {
+ parentHash := common.Hash{}
+ baseHeight := uint64(0)
+ t := s.dMoment
+ 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,
+ }
+ 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),
+ }
+ s.Require().NoError(s.signer.SignBlock(b))
+ return b
+}
+
+func (s *BlockChainTestSuite) newRandomnessFromBlock(
+ b *types.Block) *types.BlockRandomnessResult {
+ return &types.BlockRandomnessResult{
+ BlockHash: b.Hash,
+ Position: b.Position,
+ Randomness: common.GenerateRandomBytes(),
+ }
+}
+
+func (s *BlockChainTestSuite) newBlockChain(initB *types.Block) *blockChain {
+ initRound := uint64(0)
+ if initB != nil {
+ initRound = initB.Position.Round
+ }
+ initConfig := blockChainConfig{}
+ initConfig.fromConfig(initRound, &types.Config{
+ MinBlockInterval: s.blockInterval,
+ RoundInterval: s.roundInterval,
+ })
+ initConfig.setRoundBeginTime(s.dMoment)
+ return newBlockChain(s.nID, initB, initConfig, test.NewApp(0, nil),
+ &testTSigVerifierGetter{}, s.signer, &common.NullLogger{})
+}
+
+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, rands []*types.BlockRandomnessResult) {
+ var (
+ bc = s.newBlockChain(initBlock)
+ start = make(chan struct{})
+ newNotif = make(chan struct{}, 1)
+ delivered []*types.Block
+ )
+ 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.BlockRandomnessResult:
+ if err := bc.addRandomness(val); err != nil {
+ // Never assertion in sub routine when testing.
+ panic(err)
+ }
+ default:
+ panic(fmt.Errorf("unknown type: %v", v))
+ }
+ select {
+ case newNotif <- struct{}{}:
+ default:
+ }
+ }
+ for _, b := range blocks {
+ go add(b)
+ }
+ for _, r := range rands {
+ go add(r)
+ }
+ 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.Finalization.Randomness)
+ for _, bb := range delivered[1:] {
+ s.Require().Equal(b.Position.Height+1, bb.Position.Height)
+ s.Require().NotEmpty(b.Finalization.Randomness)
+ b = bb
+ }
+}
+
+func (s *BlockChainTestSuite) TestBasicUsage() {
+ initBlock := s.newRoundOneInitBlock()
+ bc := s.newBlockChain(initBlock)
+ // 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},
+ }
+ s.Require().NoError(s.signer.SignBlock(b5))
+ r0 := s.newRandomnessFromBlock(b0)
+ r1 := s.newRandomnessFromBlock(b1)
+ r2 := s.newRandomnessFromBlock(b2)
+ r3 := s.newRandomnessFromBlock(b3)
+ r4 := s.newRandomnessFromBlock(b4)
+ r5 := s.newRandomnessFromBlock(b5)
+ // add those datum in reversed order of position.
+ s.Require().NoError(bc.addRandomness(r4))
+ s.Require().NoError(bc.addRandomness(r3))
+ s.Require().NoError(bc.addRandomness(r2))
+ s.Require().NoError(bc.addRandomness(r1))
+ s.Require().NoError(bc.addRandomness(r0))
+ 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, 5)
+ s.Require().Equal(extracted[4].Hash, b4.Hash)
+ s.Require().NoError(bc.addRandomness(r5))
+ extracted = bc.extractBlocks()
+ s.Require().Len(extracted, 1)
+ s.Require().Equal(extracted[0].Hash, b5.Hash)
+}
+
+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.BlockRandomnessResult{}
+ )
+ 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)
+ // Non-zero chainID is not allowed.
+ s.Require().Panics(func() {
+ bc.sanityCheck(&types.Block{Position: types.Position{ChainID: 1}})
+ })
+ // Empty block is not allowed.
+ s.Require().Panics(func() {
+ bc.sanityCheck(&types.Block{})
+ })
+ 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())
+ // ErrIncorrectParentHash
+ b4 := &types.Block{
+ ParentHash: b2.Hash,
+ Position: types.Position{
+ Round: 1,
+ Height: 4,
+ },
+ Timestamp: b3.Timestamp.Add(1 * time.Second),
+ }
+ s.Require().NoError(s.signer.SignBlock(b4))
+ s.Require().Equal(
+ ErrIncorrectParentHash.Error(), bc.sanityCheck(b4).Error())
+ // There is no valid signature attached.
+ b4.ParentHash = b3.Hash
+ s.Require().Error(bc.sanityCheck(b4))
+ // OK case.
+ s.Require().NoError(s.signer.SignBlock(b4))
+ s.Require().NoError(bc.sanityCheck(b4))
+}
+
+func (s *BlockChainTestSuite) TestAppendConfig() {
+ bc := s.newBlockChain(nil)
+ s.Require().Equal(ErrRoundNotIncreasing.Error(),
+ bc.appendConfig(0, &types.Config{}).Error())
+ s.Require().Equal(ErrRoundNotIncreasing.Error(),
+ bc.appendConfig(2, &types.Config{}).Error())
+ s.Require().NoError(bc.appendConfig(1, &types.Config{}))
+}
+
+func (s *BlockChainTestSuite) TestConfirmed() {
+ bc := s.newBlockChain(nil)
+ 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(0))
+ s.Require().False(bc.confirmed(1))
+ s.Require().True(bc.confirmed(2))
+}
+
+func (s *BlockChainTestSuite) TestNextBlock() {
+ bc := s.newBlockChain(nil)
+ blocks := s.newBlocks(3, nil)
+ nextH, nextT := bc.nextBlock()
+ s.Require().Equal(nextH, uint64(0))
+ s.Require().Equal(nextT, bc.configs[0].roundBeginTime)
+ // Add one block.
+ s.Require().NoError(bc.addBlock(blocks[0]))
+ nextH, nextT = bc.nextBlock()
+ s.Require().Equal(nextH, uint64(1))
+ 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)
+}
+
+func (s *BlockChainTestSuite) TestPendingBlocksWithoutRandomness() {
+ initBlock := s.newRoundOneInitBlock()
+ bc := s.newBlockChain(initBlock)
+ blocks := s.newBlocks(4, initBlock)
+ s.Require().NoError(bc.addBlock(blocks[0]))
+ s.Require().NoError(bc.addBlock(blocks[1]))
+ s.Require().NoError(bc.addBlock(blocks[3]))
+ s.Require().Equal(bc.pendingBlocksWithoutRandomness(), common.Hashes{
+ blocks[0].Hash, blocks[1].Hash, blocks[3].Hash})
+ s.Require().NoError(bc.addRandomness(s.newRandomnessFromBlock(blocks[0])))
+ s.Require().Equal(bc.pendingBlocksWithoutRandomness(), common.Hashes{
+ blocks[1].Hash, blocks[3].Hash})
+}
+
+func (s *BlockChainTestSuite) TestLastXBlock() {
+ initBlock := s.newRoundOneInitBlock()
+ bc := s.newBlockChain(initBlock)
+ 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().NoError(bc.addRandomness(s.newRandomnessFromBlock(blocks[0])))
+ 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)
+ 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)
+ blocks := s.newBlocks(1, nil)
+ s.Require().NoError(bc.addBlock(blocks[0]))
+ // Add an empty block after a normal block.
+ pos := types.Position{Height: 1}
+ emptyB, err := bc.addEmptyBlock(pos)
+ s.Require().NotNil(emptyB)
+ s.Require().True(emptyB.Position.Equal(pos))
+ s.Require().NoError(err)
+ // Add an empty block after an empty block.
+ pos = types.Position{Height: 2}
+ emptyB, err = bc.addEmptyBlock(pos)
+ s.Require().NotNil(emptyB)
+ s.Require().True(emptyB.Position.Equal(pos))
+ s.Require().NoError(err)
+}
+
+func TestBlockChain(t *testing.T) {
+ suite.Run(t, new(BlockChainTestSuite))
+}