// 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 test
import (
"bytes"
"testing"
"time"
"github.com/dexon-foundation/dexon-consensus/common"
"github.com/dexon-foundation/dexon-consensus/core"
"github.com/dexon-foundation/dexon-consensus/core/types"
"github.com/stretchr/testify/suite"
)
type AppTestSuite struct {
suite.Suite
to1, to2, to3 *AppTotalOrderRecord
}
func (s *AppTestSuite) SetupSuite() {
s.to1 = &AppTotalOrderRecord{
BlockHashes: common.Hashes{
common.NewRandomHash(),
common.NewRandomHash(),
},
Mode: core.TotalOrderingModeNormal,
}
s.to2 = &AppTotalOrderRecord{
BlockHashes: common.Hashes{
common.NewRandomHash(),
common.NewRandomHash(),
common.NewRandomHash(),
},
Mode: core.TotalOrderingModeNormal,
}
s.to3 = &AppTotalOrderRecord{
BlockHashes: common.Hashes{
common.NewRandomHash(),
},
Mode: core.TotalOrderingModeNormal,
}
}
func (s *AppTestSuite) setupAppByTotalOrderDeliver(
app *App, to *AppTotalOrderRecord) {
for _, h := range to.BlockHashes {
app.BlockConfirmed(types.Block{Hash: h})
}
app.TotalOrderingDelivered(to.BlockHashes, to.Mode)
for _, h := range to.BlockHashes {
// To make it simpler, use the index of hash sequence
// as the time.
s.deliverBlockWithTimeFromSequenceLength(app, h)
}
}
func (s *AppTestSuite) deliverBlockWithTimeFromSequenceLength(
app *App, hash common.Hash) {
s.deliverBlock(app, hash, time.Time{}.Add(
time.Duration(len(app.DeliverSequence))*time.Second),
uint64(len(app.DeliverSequence)+1))
}
func (s *AppTestSuite) deliverBlock(
app *App, hash common.Hash, timestamp time.Time, height uint64) {
app.BlockDelivered(hash, types.Position{}, types.FinalizationResult{
Timestamp: timestamp,
Height: height,
})
}
func (s *AppTestSuite) TestCompare() {
req := s.Require()
app1 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app1, s.to1)
s.setupAppByTotalOrderDeliver(app1, s.to2)
s.setupAppByTotalOrderDeliver(app1, s.to3)
// An App with different deliver sequence.
app2 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app2, s.to1)
s.setupAppByTotalOrderDeliver(app2, s.to2)
hash := common.NewRandomHash()
app2.BlockConfirmed(types.Block{Hash: hash})
app2.TotalOrderingDelivered(common.Hashes{hash}, core.TotalOrderingModeNormal)
s.deliverBlockWithTimeFromSequenceLength(app2, hash)
req.Equal(ErrMismatchBlockHashSequence, app1.Compare(app2))
// An App with different consensus time for the same block.
app3 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app3, s.to1)
s.setupAppByTotalOrderDeliver(app3, s.to2)
for _, h := range s.to3.BlockHashes {
app3.BlockConfirmed(types.Block{Hash: h})
}
app3.TotalOrderingDelivered(s.to3.BlockHashes, s.to3.Mode)
wrongTime := time.Time{}.Add(
time.Duration(len(app3.DeliverSequence)) * time.Second)
wrongTime = wrongTime.Add(1 * time.Second)
s.deliverBlock(app3, s.to3.BlockHashes[0], wrongTime,
uint64(len(app3.DeliverSequence)+1))
req.Equal(ErrMismatchConsensusTime, app1.Compare(app3))
req.Equal(ErrMismatchConsensusTime, app3.Compare(app1))
// An App without any delivered blocks.
app4 := NewApp(nil)
req.Equal(ErrEmptyDeliverSequence, app4.Compare(app1))
req.Equal(ErrEmptyDeliverSequence, app1.Compare(app4))
}
func (s *AppTestSuite) TestVerify() {
req := s.Require()
// An OK App instance.
app1 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app1, s.to1)
s.setupAppByTotalOrderDeliver(app1, s.to2)
s.setupAppByTotalOrderDeliver(app1, s.to3)
req.NoError(app1.Verify())
// A delivered block without strongly ack
s.deliverBlock(app1, common.NewRandomHash(), time.Time{},
uint64(len(app1.DeliverSequence)))
req.Equal(ErrDeliveredBlockNotConfirmed, app1.Verify())
// The consensus time is out of order.
app2 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app2, s.to1)
for _, h := range s.to2.BlockHashes {
app2.BlockConfirmed(types.Block{Hash: h})
}
app2.TotalOrderingDelivered(s.to2.BlockHashes, s.to2.Mode)
s.deliverBlock(app2, s.to2.BlockHashes[0], time.Time{},
uint64(len(app2.DeliverSequence)+1))
req.Equal(ErrConsensusTimestampOutOfOrder, app2.Verify())
// A delivered block is not found in total ordering delivers.
app3 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app3, s.to1)
hash := common.NewRandomHash()
app3.BlockConfirmed(types.Block{Hash: hash})
s.deliverBlockWithTimeFromSequenceLength(app3, hash)
req.Equal(ErrMismatchTotalOrderingAndDelivered, app3.Verify())
// A delivered block is not found in total ordering delivers.
app4 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app4, s.to1)
for _, h := range s.to2.BlockHashes {
app4.BlockConfirmed(types.Block{Hash: h})
}
app4.TotalOrderingDelivered(s.to2.BlockHashes, s.to2.Mode)
hash = common.NewRandomHash()
app4.BlockConfirmed(types.Block{Hash: hash})
app4.TotalOrderingDelivered(common.Hashes{hash}, core.TotalOrderingModeNormal)
s.deliverBlockWithTimeFromSequenceLength(app4, hash)
// Witness ack on unknown block.
app5 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app5, s.to1)
// The conensus height is out of order.
app6 := NewApp(nil)
s.setupAppByTotalOrderDeliver(app6, s.to1)
for _, h := range s.to2.BlockHashes {
app6.BlockConfirmed(types.Block{Hash: h})
}
app6.TotalOrderingDelivered(s.to2.BlockHashes, s.to2.Mode)
s.deliverBlock(app6, s.to2.BlockHashes[0], time.Time{}.Add(
time.Duration(len(app6.DeliverSequence))*time.Second),
uint64(len(app6.DeliverSequence)+2))
req.Equal(ErrConsensusHeightOutOfOrder, app6.Verify())
// Test the acking block doesn't delivered.
app7 := NewApp(nil)
// Patch a block's acks.
b7 := &types.Block{
Hash: common.NewRandomHash(),
Acks: common.NewSortedHashes(common.Hashes{common.NewRandomHash()}),
}
app7.BlockConfirmed(*b7)
app7.TotalOrderingDelivered(
common.Hashes{b7.Hash}, core.TotalOrderingModeNormal)
app7.BlockDelivered(b7.Hash, types.Position{}, types.FinalizationResult{
Timestamp: time.Now(),
Height: 1,
})
req.Equal(ErrAckingBlockNotDelivered, app7.Verify())
}
func (s *AppTestSuite) TestWitness() {
// Deliver several blocks, there is only one chain only.
app := NewApp(nil)
deliver := func(b *types.Block) {
app.BlockConfirmed(*b)
app.BlockDelivered(b.Hash, b.Position, b.Finalization)
}
b00 := &types.Block{
Hash: common.NewRandomHash(),
Finalization: types.FinalizationResult{
Height: 1,
Timestamp: time.Now().UTC(),
}}
b01 := &types.Block{
Hash: common.NewRandomHash(),
Position: types.Position{Height: 1},
Finalization: types.FinalizationResult{
ParentHash: b00.Hash,
Height: 2,
Timestamp: time.Now().UTC(),
},
Witness: types.Witness{
Height: 1,
Data: b00.Hash.Bytes(),
}}
b02 := &types.Block{
Hash: common.NewRandomHash(),
Position: types.Position{Height: 2},
Finalization: types.FinalizationResult{
ParentHash: b01.Hash,
Height: 3,
Timestamp: time.Now().UTC(),
},
Witness: types.Witness{
Height: 1,
Data: b00.Hash.Bytes(),
}}
deliver(b00)
deliver(b01)
deliver(b02)
// A block with higher witness height, should retry later.
s.Require().Equal(types.VerifyRetryLater, app.VerifyBlock(&types.Block{
Witness: types.Witness{Height: 4}}))
// Mismatched witness height and data, should return invalid.
s.Require().Equal(types.VerifyInvalidBlock, app.VerifyBlock(&types.Block{
Witness: types.Witness{Height: 1, Data: b01.Hash.Bytes()}}))
// We can only verify a block followed last confirmed block.
s.Require().Equal(types.VerifyRetryLater, app.VerifyBlock(&types.Block{
Witness: types.Witness{Height: 2, Data: b01.Hash.Bytes()},
Position: types.Position{Height: 4}}))
// It's the OK case.
s.Require().Equal(types.VerifyOK, app.VerifyBlock(&types.Block{
Witness: types.Witness{Height: 2, Data: b01.Hash.Bytes()},
Position: types.Position{Height: 3}}))
// Check current last pending height.
s.Require().Equal(app.LastPendingHeight, uint64(3))
// We can only prepare witness for what've delivered.
_, err := app.PrepareWitness(4)
s.Require().IsType(err, ErrLowerPendingHeight)
// It should be ok to prepare for height that already delivered.
w, err := app.PrepareWitness(3)
s.Require().NoError(err)
s.Require().Equal(w.Height, b02.Finalization.Height)
s.Require().Equal(0, bytes.Compare(w.Data, b02.Hash[:]))
}
func TestApp(t *testing.T) {
suite.Run(t, new(AppTestSuite))
}