// Copyright 2018 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 (
"math"
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/dexon-foundation/dexon-consensus/common"
"github.com/dexon-foundation/dexon-consensus/core/types"
)
type ConsensusTimestampTest struct {
suite.Suite
}
func (s *ConsensusTimestampTest) generateBlocksWithTimestamp(
now time.Time,
blockNum, chainNum int,
step, sigma time.Duration) []*types.Block {
blocks := make([]*types.Block, blockNum)
chainIDs := make([]uint32, len(blocks))
for i := range chainIDs {
chainIDs[i] = uint32(i % chainNum)
}
rand.Shuffle(len(chainIDs), func(i, j int) {
chainIDs[i], chainIDs[j] = chainIDs[j], chainIDs[i]
})
chainTimestamps := make(map[uint32]time.Time)
for idx := range blocks {
blocks[idx] = &types.Block{}
block := blocks[idx]
if idx < chainNum {
// Genesis blocks.
block.Position.ChainID = uint32(idx)
block.ParentHash = common.Hash{}
block.Position.Height = 0
s.Require().True(block.IsGenesis())
chainTimestamps[uint32(idx)] = now
} else {
block.Position.ChainID = chainIDs[idx]
// Assign 1 to height to make this block non-genesis.
block.Position.Height = 1
s.Require().False(block.IsGenesis())
}
block.Timestamp = chainTimestamps[block.Position.ChainID]
// Update timestamp for next block.
diffSeconds := rand.NormFloat64() * sigma.Seconds()
diffSeconds = math.Min(diffSeconds, step.Seconds()/2.1)
diffSeconds = math.Max(diffSeconds, -step.Seconds()/2.1)
diffDuration := time.Duration(diffSeconds*1000) * time.Millisecond
chainTimestamps[block.Position.ChainID] =
chainTimestamps[block.Position.ChainID].Add(step).Add(diffDuration)
s.Require().True(block.Timestamp.Before(
chainTimestamps[block.Position.ChainID]))
}
return blocks
}
func (s *ConsensusTimestampTest) extractTimestamps(
blocks []*types.Block) []time.Time {
timestamps := make([]time.Time, 0, len(blocks))
for _, block := range blocks {
if block.IsGenesis() {
continue
}
timestamps = append(timestamps, block.Finalization.Timestamp)
}
return timestamps
}
// TestTimestampPartition verifies that processing segments of compatction chain
// should have the same result as processing the whole chain at once.
func (s *ConsensusTimestampTest) TestTimestampPartition() {
blockNums := []int{50, 100, 30}
chainNum := 19
sigma := 100 * time.Millisecond
totalTimestamps := make([]time.Time, 0)
now := time.Now().UTC()
ct := newConsensusTimestamp(now, 0, uint32(chainNum))
totalBlockNum := 0
for _, blockNum := range blockNums {
totalBlockNum += blockNum
}
totalChain := s.generateBlocksWithTimestamp(now,
totalBlockNum, chainNum, time.Second, sigma)
for _, blockNum := range blockNums {
var chain []*types.Block
chain, totalChain = totalChain[:blockNum], totalChain[blockNum:]
err := ct.processBlocks(chain)
s.Require().NoError(err)
timestamps := s.extractTimestamps(chain)
totalChain = append(totalChain, chain...)
totalTimestamps = append(totalTimestamps, timestamps...)
}
ct2 := newConsensusTimestamp(now, 0, uint32(chainNum))
err := ct2.processBlocks(totalChain)
s.Require().NoError(err)
timestamps2 := s.extractTimestamps(totalChain)
s.Equal(totalTimestamps, timestamps2)
}
func (s *ConsensusTimestampTest) TestTimestampIncrease() {
chainNum := 19
sigma := 100 * time.Millisecond
now := time.Now().UTC()
ct := newConsensusTimestamp(now, 0, uint32(chainNum))
chain := s.generateBlocksWithTimestamp(
now, 1000, chainNum, time.Second, sigma)
err := ct.processBlocks(chain)
s.Require().NoError(err)
timestamps := s.extractTimestamps(chain)
for i := 1; i < len(timestamps); i++ {
s.False(timestamps[i].Before(timestamps[i-1]))
}
// Test if the processBlocks is stable.
ct2 := newConsensusTimestamp(now, 0, uint32(chainNum))
ct2.processBlocks(chain)
s.Require().NoError(err)
timestamps2 := s.extractTimestamps(chain)
s.Equal(timestamps, timestamps2)
}
func (s *ConsensusTimestampTest) TestTimestampConfigChange() {
chainNum := 19
sigma := 100 * time.Millisecond
now := time.Now().UTC()
ct := newConsensusTimestamp(now, 20, uint32(chainNum))
chain := s.generateBlocksWithTimestamp(now,
1000, chainNum, time.Second, sigma)
blocks := make([]*types.Block, 0, 1000)
ct.appendConfig(21, &types.Config{NumChains: uint32(16)})
ct.appendConfig(22, &types.Config{NumChains: uint32(19)})
// Blocks 0 to 299 is in round 20, blocks 300 to 599 is in round 21 and ignore
// blocks which ChainID is 16 to 18, blocks 600 to 999 is in round 22.
for i := 0; i < 1000; i++ {
add := true
if i < 300 {
chain[i].Position.Round = 20
} else if i < 600 {
chain[i].Position.Round = 21
add = chain[i].Position.ChainID < 16
} else {
chain[i].Position.Round = 22
}
if add {
blocks = append(blocks, chain[i])
}
}
err := ct.processBlocks(blocks)
s.Require().NoError(err)
}
func (s *ConsensusTimestampTest) TestTimestampRoundInterleave() {
chainNum := 9
sigma := 100 * time.Millisecond
now := time.Now().UTC()
ct := newConsensusTimestamp(now, 0, uint32(chainNum))
ct.appendConfig(1, &types.Config{NumChains: uint32(chainNum)})
chain := s.generateBlocksWithTimestamp(now,
100, chainNum, time.Second, sigma)
for i := 50; i < 100; i++ {
chain[i].Position.Round = 1
}
chain[48].Position.Round = 1
chain[49].Position.Round = 1
chain[50].Position.Round = 0
chain[51].Position.Round = 0
err := ct.processBlocks(chain)
s.Require().NoError(err)
}
func (s *ConsensusTimestampTest) TestTimestampSync() {
chainNum := 19
sigma := 100 * time.Millisecond
now := time.Now().UTC()
ct := newConsensusTimestamp(now, 0, uint32(chainNum))
chain := s.generateBlocksWithTimestamp(now,
100, chainNum, time.Second, sigma)
err := ct.processBlocks(chain[:chainNum-1])
s.Require().NoError(err)
s.Require().False(ct.isSynced())
err = ct.processBlocks(chain[chainNum-1:])
s.Require().NoError(err)
s.Require().True(ct.isSynced())
}
func TestConsensusTimestamp(t *testing.T) {
suite.Run(t, new(ConsensusTimestampTest))
}