// 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
// <http://www.gnu.org/licenses/>.
package core
import (
"math/rand"
"sort"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/dexon-foundation/dexon-consensus-core/common"
"github.com/dexon-foundation/dexon-consensus-core/core/blockdb"
"github.com/dexon-foundation/dexon-consensus-core/core/test"
"github.com/dexon-foundation/dexon-consensus-core/core/types"
)
type BlockLatticeTest struct {
suite.Suite
}
// hashBlock is a helper to hash a block and check if any error.
func (s *BlockLatticeTest) hashBlock(b *types.Block) {
var err error
b.Hash, err = hashBlock(b)
s.Require().Nil(err)
}
func (s *BlockLatticeTest) prepareGenesisBlock(
chainID uint32) (b *types.Block) {
b = &types.Block{
ParentHash: common.Hash{},
Position: types.Position{
ChainID: chainID,
Height: 0,
},
Acks: common.NewSortedHashes(common.Hashes{}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
return
}
// genTestCase1 generates test case 1,
// 3
// |
// 2
// | \
// 1 | 1
// | | |
// 0 0 0 0 (block height)
// 0 1 2 3 (validator)
func (s *BlockLatticeTest) genTestCase1() (bl *blockLattice) {
// Create new reliableBroadcast instance with 4 validators
var (
b *types.Block
delivered []*types.Block
h common.Hash
chainNum uint32 = 4
req = s.Require()
err error
)
bl = newBlockLattice(0, chainNum)
// Add genesis blocks.
for i := uint32(0); i < chainNum; i++ {
b = s.prepareGenesisBlock(i)
delivered, err = bl.addBlock(b)
// Genesis blocks are safe to be added to DAG, they acks no one.
req.Len(delivered, 1)
req.Nil(err)
}
// Add block 0-1 which acks 0-0.
h = bl.chains[0].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Hash: common.NewRandomHash(),
Timestamp: time.Now().UTC(),
Position: types.Position{
ChainID: 0,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
}
s.hashBlock(b)
delivered, err = bl.addBlock(b)
req.Len(delivered, 1)
req.Equal(delivered[0].Hash, b.Hash)
req.Nil(err)
req.NotNil(bl.chains[0].getBlockByHeight(1))
// Add block 0-2 which acks 0-1 and 1-0.
h = bl.chains[0].getBlockByHeight(1).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 0,
Height: 2,
},
Timestamp: time.Now().UTC(),
Acks: common.NewSortedHashes(common.Hashes{
h,
bl.chains[1].getBlockByHeight(0).Hash,
}),
}
s.hashBlock(b)
delivered, err = bl.addBlock(b)
req.Len(delivered, 1)
req.Equal(delivered[0].Hash, b.Hash)
req.Nil(err)
req.NotNil(bl.chains[0].getBlockByHeight(2))
// Add block 0-3 which acks 0-2.
h = bl.chains[0].getBlockByHeight(2).Hash
b = &types.Block{
ParentHash: h,
Hash: common.NewRandomHash(),
Timestamp: time.Now().UTC(),
Position: types.Position{
ChainID: 0,
Height: 3,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
}
s.hashBlock(b)
delivered, err = bl.addBlock(b)
req.Len(delivered, 1)
req.Equal(delivered[0].Hash, b.Hash)
req.Nil(err)
req.NotNil(bl.chains[0].getBlockByHeight(3))
// Add block 3-1 which acks 3-0.
h = bl.chains[3].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Hash: common.NewRandomHash(),
Timestamp: time.Now().UTC(),
Position: types.Position{
ChainID: 3,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
}
s.hashBlock(b)
delivered, err = bl.addBlock(b)
req.Len(delivered, 1)
req.Equal(delivered[0].Hash, b.Hash)
req.Nil(err)
req.NotNil(bl.chains[3].getBlockByHeight(0))
return
}
func (s *BlockLatticeTest) TestSanityCheck() {
var (
b *types.Block
h common.Hash
bl = s.genTestCase1()
req = s.Require()
err error
)
// Non-genesis block with no ack, should get error.
b = &types.Block{
ParentHash: common.NewRandomHash(),
Position: types.Position{
ChainID: 0,
Height: 10,
},
Acks: common.NewSortedHashes(common.Hashes{}),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(ErrNotAckParent.Error(), err.Error())
// Non-genesis block which acks its parent but the height is invalid.
h = bl.chains[1].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 1,
Height: 2,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(ErrInvalidBlockHeight.Error(), err.Error())
// Invalid chain ID.
h = bl.chains[1].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 100,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(ErrInvalidChainID.Error(), err.Error())
// Fork block.
h = bl.chains[0].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 0,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(ErrForkBlock.Error(), err.Error())
// Replicated ack.
h = bl.chains[0].getBlockByHeight(3).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 0,
Height: 4,
},
Acks: common.NewSortedHashes(common.Hashes{
h,
bl.chains[1].getBlockByHeight(0).Hash,
}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(ErrDoubleAck.Error(), err.Error())
// Acking block doesn't exists.
h = bl.chains[1].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 1,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{
h,
common.NewRandomHash(),
}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(err.Error(), ErrAckingBlockNotExists.Error())
// Parent block on different chain.
h = bl.chains[1].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 2,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{
h,
bl.chains[2].getBlockByHeight(0).Hash,
}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(err.Error(), ErrInvalidParentChain.Error())
// Ack two blocks on the same chain.
h = bl.chains[2].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 2,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{
h,
bl.chains[0].getBlockByHeight(0).Hash,
bl.chains[0].getBlockByHeight(1).Hash,
}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
err = bl.sanityCheck(b)
req.NotNil(err)
req.Equal(err.Error(), ErrDuplicatedAckOnOneChain.Error())
// Normal block.
h = bl.chains[1].getBlockByHeight(0).Hash
b = &types.Block{
ParentHash: h,
Position: types.Position{
ChainID: 1,
Height: 1,
},
Acks: common.NewSortedHashes(common.Hashes{h}),
Timestamp: time.Now().UTC(),
}
s.hashBlock(b)
req.Nil(bl.sanityCheck(b))
}
func (s *BlockLatticeTest) TestAreAllAcksInLattice() {
var (
b *types.Block
bl = s.genTestCase1()
req = s.Require()
)
// Empty ack should get true, although won't pass sanity check.
b = &types.Block{
Acks: common.NewSortedHashes(common.Hashes{}),
}
req.True(bl.areAllAcksInLattice(b))
// Acks blocks in lattice
b = &types.Block{
Acks: common.NewSortedHashes(common.Hashes{
bl.chains[0].getBlockByHeight(0).Hash,
bl.chains[0].getBlockByHeight(1).Hash,
}),
}
req.True(bl.areAllAcksInLattice(b))
// Acks random block hash.
b = &types.Block{
Acks: common.NewSortedHashes(common.Hashes{common.NewRandomHash()}),
}
req.False(bl.areAllAcksInLattice(b))
}
func (s *BlockLatticeTest) TestRandomIntensiveAcking() {
var (
chainNum uint32 = 19
bl = newBlockLattice(0, chainNum)
req = s.Require()
delivered []*types.Block
extracted []*types.Block
b *types.Block
err error
)
// Generate genesis blocks.
for i := uint32(0); i < chainNum; i++ {
b = s.prepareGenesisBlock(i)
delivered, err = bl.addBlock(b)
req.Len(delivered, 1)
req.Nil(err)
}
for i := 0; i < 5000; i++ {
b := &types.Block{
Position: types.Position{
ChainID: uint32(rand.Intn(int(chainNum))),
},
Timestamp: time.Now().UTC(),
}
bl.prepareBlock(b)
s.hashBlock(b)
delivered, err = bl.addBlock(b)
req.Nil(err)
extracted = append(extracted, delivered...)
}
// The len of array extractedBlocks should be about 5000.
req.True(len(extracted) > 4500)
// The len of bl.blockInfos should be small if deleting mechanism works.
req.True(len(bl.blockByHash) < 500)
}
func (s *BlockLatticeTest) TestRandomlyGeneratedBlocks() {
var (
chainNum uint32 = 19
blockNum = 50
repeat = 20
delivered []*types.Block
err error
req = s.Require()
blocklattices []*blockLattice
)
// Prepare a randomly generated blocks.
db, err := blockdb.NewMemBackedBlockDB()
req.Nil(err)
gen := test.NewBlocksGenerator(nil, hashBlock)
_, err = gen.Generate(chainNum, blockNum, nil, db)
req.Nil(err)
iter, err := db.GetAll()
req.Nil(err)
// Setup a revealer that would reveal blocks randomly but still form
// valid DAG without holes.
revealer, err := test.NewRandomDAGRevealer(iter)
req.Nil(err)
revealedHashesAsString := map[string]struct{}{}
deliveredHashesAsString := map[string]struct{}{}
for i := 0; i < repeat; i++ {
bl := newBlockLattice(0, chainNum)
deliveredHashes := common.Hashes{}
revealedHashes := 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)
revealedHashes = append(revealedHashes, b.Hash)
// Pass blocks to blocklattice.
delivered, err = bl.addBlock(&b)
req.Nil(err)
for _, b := range delivered {
deliveredHashes = append(deliveredHashes, b.Hash)
}
}
// To make it easier to check, sort hashes of
// strongly acked blocks, and concatenate them into
// a string.
sort.Sort(deliveredHashes)
asString := ""
for _, h := range deliveredHashes {
asString += h.String() + ","
}
deliveredHashesAsString[asString] = struct{}{}
// Compose revealing hash sequense to string.
asString = ""
for _, h := range revealedHashes {
asString += h.String() + ","
}
revealedHashesAsString[asString] = struct{}{}
blocklattices = append(blocklattices, bl)
}
// Make sure concatenated hashes of strongly acked blocks are identical.
req.Len(deliveredHashesAsString, 1)
for h := range deliveredHashesAsString {
// Make sure at least some blocks are strongly acked.
req.True(len(h) > 0)
}
// Make sure we test for more than 1 revealing sequence.
req.True(len(revealedHashesAsString) > 1)
// Make sure each blocklattice instance have identical working set.
req.True(len(blocklattices) >= repeat)
for i, bI := range blocklattices {
for j, bJ := range blocklattices {
if i == j {
continue
}
for chainID, statusI := range bI.chains {
req.Equal(statusI.minHeight, bJ.chains[chainID].minHeight)
req.Equal(statusI.nextOutput, bJ.chains[chainID].nextOutput)
req.Equal(len(statusI.blocks), len(bJ.chains[chainID].blocks))
// Check nextAck.
for x, ackI := range statusI.nextAck {
req.Equal(ackI, bJ.chains[chainID].nextAck[x])
}
// Check blocks.
if len(statusI.blocks) > 0 {
req.Equal(statusI.blocks[0], bJ.chains[chainID].blocks[0])
}
}
// Check blockByHash.
req.Equal(bI.blockByHash, bJ.blockByHash)
}
}
}
func (s *BlockLatticeTest) TestPrepareBlock() {
var (
chainNum uint32 = 4
req = s.Require()
bl = newBlockLattice(0, chainNum)
minInterval = 50 * time.Millisecond
delivered []*types.Block
err error
)
// Setup genesis blocks.
b00 := s.prepareGenesisBlock(0)
time.Sleep(minInterval)
b10 := s.prepareGenesisBlock(1)
time.Sleep(minInterval)
b20 := s.prepareGenesisBlock(2)
time.Sleep(minInterval)
b30 := s.prepareGenesisBlock(3)
// Submit these blocks to blocklattice.
delivered, err = bl.addBlock(b00)
req.Len(delivered, 1)
req.Nil(err)
delivered, err = bl.addBlock(b10)
req.Len(delivered, 1)
req.Nil(err)
delivered, err = bl.addBlock(b20)
req.Len(delivered, 1)
req.Nil(err)
delivered, err = bl.addBlock(b30)
req.Len(delivered, 1)
req.Nil(err)
// We should be able to collect all 4 genesis blocks by calling
// prepareBlock.
b11 := &types.Block{
Position: types.Position{
ChainID: 1,
},
Timestamp: time.Now().UTC(),
}
bl.prepareBlock(b11)
s.hashBlock(b11)
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.ParentHash, b10.Hash)
req.Equal(b11.Position.Height, uint64(1))
delivered, err = bl.addBlock(b11)
req.Len(delivered, 1)
req.Nil(err)
// Propose/Process a block based on collected info.
b12 := &types.Block{
Position: types.Position{
ChainID: 1,
},
Timestamp: time.Now().UTC(),
}
bl.prepareBlock(b12)
s.hashBlock(b12)
// 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{
Position: types.Position{
ChainID: 0,
},
}
bl.prepareBlock(b01)
s.hashBlock(b01)
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 (s *BlockLatticeTest) TestCalcPurgeHeight() {
// Test chainStatus.calcPurgeHeight, we don't have
// to prepare blocks to test it.
var req = s.Require()
chain := &chainStatus{
minHeight: 0,
nextOutput: 0,
nextAck: []uint64{1, 1, 1, 1},
}
// When calculated safe is underflow, nok.
safe, ok := chain.calcPurgeHeight()
req.False(ok)
// height=1 is outputed, and acked by everyone else.
chain.nextOutput = 1
safe, ok = chain.calcPurgeHeight()
req.True(ok)
req.Equal(safe, uint64(0))
// Should take nextAck's height into consideration.
chain.nextOutput = 2
safe, ok = chain.calcPurgeHeight()
req.True(ok)
req.Equal(safe, uint64(0))
// When minHeight is large that safe height, return nok.
chain.minHeight = 1
chain.nextOutput = 1
safe, ok = chain.calcPurgeHeight()
req.False(ok)
}
func (s *BlockLatticeTest) TestPurge() {
// Make a simplest test case to test chainStatus.purge.
// Make sure status after purge 1 block expected.
b00 := &types.Block{Hash: common.NewRandomHash()}
b01 := &types.Block{Hash: common.NewRandomHash()}
b02 := &types.Block{Hash: common.NewRandomHash()}
chain := &chainStatus{
blocks: []*types.Block{b00, b01, b02},
nextAck: []uint64{1, 1, 1, 1},
nextOutput: 1,
}
hashes := chain.purge()
s.Equal(hashes, common.Hashes{b00.Hash})
s.Equal(chain.minHeight, uint64(1))
s.Require().Len(chain.blocks, 2)
s.Equal(chain.blocks[0].Hash, b01.Hash)
s.Equal(chain.blocks[1].Hash, b02.Hash)
}
func TestBlockLattice(t *testing.T) {
suite.Run(t, new(BlockLatticeTest))
}