aboutsummaryrefslogtreecommitdiffstats
path: root/core/test
diff options
context:
space:
mode:
authorMission Liao <mission.liao@dexon.org>2018-08-15 23:10:21 +0800
committerGitHub <noreply@github.com>2018-08-15 23:10:21 +0800
commitd3107b56cbef1f05baddb64880c3e97d7eda87a4 (patch)
treeb94d05f101f61c9808ae1681a28dde4c37a5068f /core/test
parent39f1d8ae529805fa410d3ed08358c568343705a5 (diff)
downloaddexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.tar
dexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.tar.gz
dexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.tar.bz2
dexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.tar.lz
dexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.tar.xz
dexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.tar.zst
dexon-consensus-d3107b56cbef1f05baddb64880c3e97d7eda87a4.zip
test: add test.Scheduler (#58)
When simulating execution of core.Consensus by passing packets through golang-channel or real-socket, we need to utilize time.Sleep and time.Now to simulate the required network/proposing latency. It's problematic when we try to test a simulation with long network latency. Instead, Scheduler would try to execute the event with minimum timestamp, thus time.Sleep is replaced with Scheduler.nextTick, and time.Now is replaced with Event.Time. Changes: - Add test.Scheduler. - Add test.Stopper interface to provide encapsulate different stop conditions for scheduler. - Add a reference implementation for test.Stopper, it will stop scheduler when all validators confirmed X blocks proposed from themselves. - Add a test scenario on core.Consensus that all validators are not byzantine.
Diffstat (limited to 'core/test')
-rw-r--r--core/test/app.go12
-rw-r--r--core/test/governance.go21
-rw-r--r--core/test/interface.go29
-rw-r--r--core/test/scheduler-event.go76
-rw-r--r--core/test/scheduler.go214
-rw-r--r--core/test/scheduler_test.go175
-rw-r--r--core/test/stopper.go86
-rw-r--r--core/test/stopper_test.go103
8 files changed, 711 insertions, 5 deletions
diff --git a/core/test/app.go b/core/test/app.go
index aedcba9..5c438a7 100644
--- a/core/test/app.go
+++ b/core/test/app.go
@@ -191,3 +191,15 @@ Loop:
}
return nil
}
+
+// Check provides a backdoor to check status of App with reader lock.
+func (app *App) Check(checker func(*App)) {
+ app.ackedLock.RLock()
+ defer app.ackedLock.RUnlock()
+ app.totalOrderedLock.RLock()
+ defer app.totalOrderedLock.RUnlock()
+ app.deliveredLock.RLock()
+ defer app.deliveredLock.RUnlock()
+
+ checker(app)
+}
diff --git a/core/test/governance.go b/core/test/governance.go
index a4e86d1..58ab582 100644
--- a/core/test/governance.go
+++ b/core/test/governance.go
@@ -18,12 +18,20 @@
package test
import (
+ "fmt"
+
"github.com/dexon-foundation/dexon-consensus-core/core/types"
"github.com/dexon-foundation/dexon-consensus-core/crypto"
"github.com/dexon-foundation/dexon-consensus-core/crypto/eth"
"github.com/shopspring/decimal"
)
+var (
+ // ErrPrivateKeyNotExists means caller request private key for an
+ // unknown validator ID.
+ ErrPrivateKeyNotExists = fmt.Errorf("private key not exists")
+)
+
// Governance is an implementation of Goverance for testing purpose.
type Governance struct {
BlockProposingInterval int
@@ -79,3 +87,16 @@ func (g *Governance) GetConfigurationChangeEvent(
epoch int) []types.ConfigurationChangeEvent {
return nil
}
+
+// GetPrivateKey return the private key for that validator, this function
+// is a test utility and not a general core.Governance interface.
+func (g *Governance) GetPrivateKey(
+ vID types.ValidatorID) (key crypto.PrivateKey, err error) {
+
+ key, exists := g.PrivateKeys[vID]
+ if !exists {
+ err = ErrPrivateKeyNotExists
+ return
+ }
+ return
+}
diff --git a/core/test/interface.go b/core/test/interface.go
index 6c7b22e..0e963fd 100644
--- a/core/test/interface.go
+++ b/core/test/interface.go
@@ -1,13 +1,13 @@
// 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
+// 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
+// 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.
//
@@ -17,7 +17,10 @@
package test
-import "github.com/dexon-foundation/dexon-consensus-core/blockdb"
+import (
+ "github.com/dexon-foundation/dexon-consensus-core/blockdb"
+ "github.com/dexon-foundation/dexon-consensus-core/core/types"
+)
// Revealer defines the interface to reveal a group
// of pre-generated blocks.
@@ -27,3 +30,19 @@ type Revealer interface {
// Reset the revealing.
Reset()
}
+
+// Stopper defines an interface for Scheduler to tell when to stop execution.
+type Stopper interface {
+ // ShouldStop is provided with the ID of the handler just finishes an event.
+ // It's thread-safe to access internal/shared state of the handler at this
+ // moment.
+ // The Stopper should check state of that handler and return 'true'
+ // if the execution could be stopped.
+ ShouldStop(vID types.ValidatorID) bool
+}
+
+// EventHandler defines an interface to handle a Scheduler event.
+type EventHandler interface {
+ // Handle the event belongs to this handler, and return derivated events.
+ Handle(*Event) []*Event
+}
diff --git a/core/test/scheduler-event.go b/core/test/scheduler-event.go
new file mode 100644
index 0000000..60411b4
--- /dev/null
+++ b/core/test/scheduler-event.go
@@ -0,0 +1,76 @@
+// 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 test
+
+import (
+ "time"
+
+ "github.com/dexon-foundation/dexon-consensus-core/core/types"
+)
+
+// Event defines a scheduler event.
+type Event struct {
+ // ValidatorID is the ID of handler that this event deginated to.
+ ValidatorID types.ValidatorID
+ // Time is the expected execution time of this event.
+ Time time.Time
+ // ExecError record the error when handling this event.
+ ExecError error
+ // Payload is application specific data carried by this event.
+ Payload interface{}
+ // ParentTime is the time of parent event, this field is essential when
+ // we need to calculate the latency the handler assigned.
+ ParentTime time.Time
+ // ExecInterval is the latency to execute this event
+ ExecInterval time.Duration
+}
+
+// eventQueue implements heap.Interface.
+type eventQueue []*Event
+
+func (eq eventQueue) Len() int { return len(eq) }
+
+func (eq eventQueue) Less(i, j int) bool {
+ return eq[i].Time.Before(eq[j].Time)
+}
+
+func (eq eventQueue) Swap(i, j int) {
+ eq[i], eq[j] = eq[j], eq[i]
+}
+
+func (eq *eventQueue) Push(x interface{}) {
+ *eq = append(*eq, x.(*Event))
+}
+
+func (eq *eventQueue) Pop() interface{} {
+ pos := len(*eq) - 1
+ item := (*eq)[pos]
+ *eq = (*eq)[0:pos]
+ return item
+}
+
+// NewEvent is the constructor for Event.
+func NewEvent(
+ vID types.ValidatorID, when time.Time, payload interface{}) *Event {
+
+ return &Event{
+ ValidatorID: vID,
+ Time: when,
+ Payload: payload,
+ }
+}
diff --git a/core/test/scheduler.go b/core/test/scheduler.go
new file mode 100644
index 0000000..023d09f
--- /dev/null
+++ b/core/test/scheduler.go
@@ -0,0 +1,214 @@
+// 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 test
+
+import (
+ "container/heap"
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/dexon-foundation/dexon-consensus-core/core/types"
+)
+
+var (
+ // ErrSchedulerAlreadyStarted means callers attempt to insert some
+ // seed events after calling 'Run'.
+ ErrSchedulerAlreadyStarted = fmt.Errorf("scheduler already started")
+ // errNilEventWhenNotified is an internal error which means a worker routine
+ // can't get an event when notified.
+ errNilEventWhenNotified = fmt.Errorf("nil event when notified")
+)
+
+type schedulerHandlerRecord struct {
+ handler EventHandler
+ lock sync.Mutex
+}
+
+// Scheduler is an event scheduler.
+type Scheduler struct {
+ events eventQueue
+ eventsLock sync.Mutex
+ history []*Event
+ historyLock sync.RWMutex
+ isStarted bool
+ handlers map[types.ValidatorID]*schedulerHandlerRecord
+ handlersLock sync.RWMutex
+ eventNotification chan struct{}
+ ctx context.Context
+ cancelFunc context.CancelFunc
+ stopper Stopper
+}
+
+// NewScheduler constructs an Scheduler instance.
+func NewScheduler(stopper Stopper) *Scheduler {
+ ctx, cancel := context.WithCancel(context.Background())
+ return &Scheduler{
+ events: eventQueue{},
+ history: []*Event{},
+ handlers: make(map[types.ValidatorID]*schedulerHandlerRecord),
+ eventNotification: make(chan struct{}, 100000),
+ ctx: ctx,
+ cancelFunc: cancel,
+ stopper: stopper,
+ }
+}
+
+// Run would run the scheduler. If you need strict incrememtal execution order
+// of events based on their 'Time' field, assign 'numWorkers' as 1. If you need
+// faster execution, assign 'numWorkers' a larger number.
+func (sch *Scheduler) Run(numWorkers int) {
+ var wg sync.WaitGroup
+
+ sch.isStarted = true
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go sch.workerRoutine(&wg)
+ }
+ // Blocks until all routines are finished.
+ wg.Wait()
+}
+
+// Seed is used to provide the scheduler some seed events.
+func (sch *Scheduler) Seed(e *Event) error {
+ sch.eventsLock.Lock()
+ defer sch.eventsLock.Unlock()
+
+ if sch.isStarted {
+ return ErrSchedulerAlreadyStarted
+ }
+ sch.addEvent(e)
+ return nil
+}
+
+// RegisterEventHandler register an event handler by providing ID of
+// corresponding validator.
+func (sch *Scheduler) RegisterEventHandler(
+ vID types.ValidatorID,
+ handler EventHandler) {
+
+ sch.handlersLock.Lock()
+ defer sch.handlersLock.Unlock()
+
+ sch.handlers[vID] = &schedulerHandlerRecord{handler: handler}
+}
+
+// nextTick would pick the oldest event from eventQueue.
+func (sch *Scheduler) nextTick() (e *Event) {
+ sch.eventsLock.Lock()
+ defer sch.eventsLock.Unlock()
+
+ if len(sch.events) == 0 {
+ return nil
+ }
+ return heap.Pop(&sch.events).(*Event)
+}
+
+// addEvent is an helper function to add events into eventQueue sorted by
+// their 'Time' field.
+func (sch *Scheduler) addEvent(e *Event) {
+ // Perform sorted insertion.
+ heap.Push(&sch.events, e)
+ sch.eventNotification <- struct{}{}
+}
+
+// CloneExecutionHistory returns a cloned event execution history.
+func (sch *Scheduler) CloneExecutionHistory() (cloned []*Event) {
+ sch.historyLock.RLock()
+ defer sch.historyLock.RUnlock()
+
+ cloned = make([]*Event, len(sch.history))
+ copy(cloned, sch.history)
+ return
+}
+
+// workerRoutine is the mainloop when handling events.
+func (sch *Scheduler) workerRoutine(wg *sync.WaitGroup) {
+ defer wg.Done()
+
+ handleEvent := func(e *Event) {
+ // Find correspond handler record.
+ hRec := func(vID types.ValidatorID) *schedulerHandlerRecord {
+ sch.handlersLock.RLock()
+ defer sch.handlersLock.RUnlock()
+
+ return sch.handlers[vID]
+ }(e.ValidatorID)
+
+ newEvents := func() []*Event {
+ // This lock makes sure there would be no concurrent access
+ // against each handler.
+ hRec.lock.Lock()
+ defer hRec.lock.Unlock()
+
+ // Handle incoming event, and record its execution time.
+ beforeExecution := time.Now().UTC()
+ newEvents := hRec.handler.Handle(e)
+ e.ExecInterval = time.Now().UTC().Sub(beforeExecution)
+ // It's safe to check status of that validator under 'hRec.lock'.
+ if sch.stopper.ShouldStop(e.ValidatorID) {
+ sch.cancelFunc()
+ }
+ // Include the execution interval of parent event to the expected time
+ // to execute child events.
+ for _, newEvent := range newEvents {
+ newEvent.ParentTime = e.Time
+ newEvent.Time = newEvent.Time.Add(e.ExecInterval)
+ }
+ return newEvents
+ }()
+ // Record executed events as history.
+ func() {
+ sch.historyLock.Lock()
+ defer sch.historyLock.Unlock()
+
+ sch.history = append(sch.history, e)
+ }()
+ // Add derivated events back to event queue.
+ func() {
+ sch.eventsLock.Lock()
+ defer sch.eventsLock.Unlock()
+
+ for _, newEvent := range newEvents {
+ sch.addEvent(newEvent)
+ }
+ }()
+ }
+
+Done:
+ for {
+ // We favor scheduler-shutdown signal than other events.
+ select {
+ case <-sch.ctx.Done():
+ break Done
+ default:
+ }
+ // Block until new event arrival or scheduler shutdown.
+ select {
+ case <-sch.eventNotification:
+ e := sch.nextTick()
+ if e == nil {
+ panic(errNilEventWhenNotified)
+ }
+ handleEvent(e)
+ case <-sch.ctx.Done():
+ break Done
+ }
+ }
+}
diff --git a/core/test/scheduler_test.go b/core/test/scheduler_test.go
new file mode 100644
index 0000000..c67240f
--- /dev/null
+++ b/core/test/scheduler_test.go
@@ -0,0 +1,175 @@
+// 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 test
+
+import (
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/dexon-foundation/dexon-consensus-core/common"
+ "github.com/dexon-foundation/dexon-consensus-core/core/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type SchedulerTestSuite struct {
+ suite.Suite
+}
+
+type simpleStopper struct {
+ lock sync.Mutex
+ touched map[types.ValidatorID]int
+ touchedCount int
+}
+
+func newSimpleStopper(
+ validators []types.ValidatorID, touchedCount int) *simpleStopper {
+
+ touched := make(map[types.ValidatorID]int)
+ for _, vID := range validators {
+ touched[vID] = 0
+ }
+ return &simpleStopper{
+ touched: touched,
+ touchedCount: touchedCount,
+ }
+}
+
+func (stopper *simpleStopper) ShouldStop(vID types.ValidatorID) bool {
+ stopper.lock.Lock()
+ defer stopper.lock.Unlock()
+
+ stopper.touched[vID] = stopper.touched[vID] + 1
+ for _, count := range stopper.touched {
+ if count < stopper.touchedCount {
+ return false
+ }
+ }
+ return true
+}
+
+type simpleHandler struct {
+ count int
+ vID types.ValidatorID
+}
+
+func (handler *simpleHandler) Handle(e *Event) (events []*Event) {
+ if e.ValidatorID == handler.vID {
+ handler.count++
+ }
+ return
+}
+
+type fixedLatencyHandler struct {
+ vID types.ValidatorID
+}
+
+func (handler *fixedLatencyHandler) Handle(e *Event) (events []*Event) {
+ // Simulate execution time.
+ time.Sleep(500 * time.Millisecond)
+ return []*Event{&Event{
+ ValidatorID: handler.vID,
+ Time: e.Time.Add(800 * time.Millisecond),
+ }}
+}
+
+func (s *SchedulerTestSuite) TestEventSequence() {
+ // This test case makes sure the event sequence is correctly increment
+ // by their timestamps in 'Time' field.
+ var (
+ sch = NewScheduler(nil)
+ req = s.Require()
+ )
+
+ req.NotNil(sch)
+ now := time.Now()
+ req.Nil(sch.Seed(&Event{Time: now.Add(100 * time.Second), Payload: 1}))
+ req.Nil(sch.Seed(&Event{Time: now.Add(99 * time.Second), Payload: 2}))
+ req.Nil(sch.Seed(&Event{Time: now.Add(98 * time.Second), Payload: 3}))
+ req.Nil(sch.Seed(&Event{Time: now.Add(97 * time.Second), Payload: 4}))
+ req.Nil(sch.Seed(&Event{Time: now.Add(96 * time.Second), Payload: 5}))
+
+ req.Equal(sch.nextTick().Payload.(int), 5)
+ req.Equal(sch.nextTick().Payload.(int), 4)
+ req.Equal(sch.nextTick().Payload.(int), 3)
+ req.Equal(sch.nextTick().Payload.(int), 2)
+ req.Equal(sch.nextTick().Payload.(int), 1)
+ req.Nil(sch.nextTick())
+}
+
+func (s *SchedulerTestSuite) TestBasicRound() {
+ // This test case makes sure these facts:
+ // - event is dispatched by validatorID attached to each handler.
+ // - stopper can stop the execution when condition is met.
+ var (
+ req = s.Require()
+ validators = GenerateRandomValidatorIDs(3)
+ stopper = newSimpleStopper(validators, 2)
+ sch = NewScheduler(stopper)
+ handlers = make(map[types.ValidatorID]*simpleHandler)
+ )
+
+ for _, vID := range validators {
+ handler := &simpleHandler{vID: vID}
+ handlers[vID] = handler
+ sch.RegisterEventHandler(vID, handler)
+ req.Nil(sch.Seed(&Event{ValidatorID: vID}))
+ req.Nil(sch.Seed(&Event{ValidatorID: vID}))
+ }
+ sch.Run(10)
+ // Verify result.
+ for _, h := range handlers {
+ req.Equal(h.count, 2)
+ }
+}
+
+func (s *SchedulerTestSuite) TestChildEvent() {
+ // This test case makes sure these fields of child events are
+ // assigned correctly.
+ var (
+ req = s.Require()
+ vID = types.ValidatorID{Hash: common.NewRandomHash()}
+ stopper = newSimpleStopper(types.ValidatorIDs{vID}, 3)
+ handler = &fixedLatencyHandler{vID: vID}
+ sch = NewScheduler(stopper)
+ )
+
+ sch.RegisterEventHandler(vID, handler)
+ req.Nil(sch.Seed(&Event{
+ ValidatorID: vID,
+ Time: time.Now().UTC(),
+ }))
+ sch.Run(1)
+ // Verify result.
+ history := sch.CloneExecutionHistory()
+ req.Len(history, 3)
+ curEvent := history[0]
+ for _, e := range history[1:] {
+ // Make sure the time difference between events are more than
+ // 1.3 second.
+ req.True(e.Time.Sub(curEvent.Time) >= 1300*time.Millisecond)
+ // Make sure ParentTime field is set and is equal to parent event's
+ // time.
+ req.Equal(e.ParentTime, curEvent.Time)
+ curEvent = e
+ }
+}
+
+func TestScheduler(t *testing.T) {
+ suite.Run(t, new(SchedulerTestSuite))
+}
diff --git a/core/test/stopper.go b/core/test/stopper.go
new file mode 100644
index 0000000..da4d205
--- /dev/null
+++ b/core/test/stopper.go
@@ -0,0 +1,86 @@
+// 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 test
+
+import (
+ "sync"
+
+ "github.com/dexon-foundation/dexon-consensus-core/blockdb"
+ "github.com/dexon-foundation/dexon-consensus-core/core/types"
+)
+
+// StopByConfirmedBlocks would make sure each validators confirms
+// at least X blocks proposed by itself.
+type StopByConfirmedBlocks struct {
+ apps map[types.ValidatorID]*App
+ dbs map[types.ValidatorID]blockdb.BlockDatabase
+ lastCheckDelivered map[types.ValidatorID]int
+ confirmedBlocks map[types.ValidatorID]int
+ blockCount int
+ lock sync.Mutex
+}
+
+// NewStopByConfirmedBlocks construct an StopByConfirmedBlocks instance.
+func NewStopByConfirmedBlocks(
+ blockCount int,
+ apps map[types.ValidatorID]*App,
+ dbs map[types.ValidatorID]blockdb.BlockDatabase) *StopByConfirmedBlocks {
+
+ confirmedBlocks := make(map[types.ValidatorID]int)
+ for vID := range apps {
+ confirmedBlocks[vID] = 0
+ }
+ return &StopByConfirmedBlocks{
+ apps: apps,
+ dbs: dbs,
+ lastCheckDelivered: make(map[types.ValidatorID]int),
+ confirmedBlocks: confirmedBlocks,
+ blockCount: blockCount,
+ }
+}
+
+// ShouldStop implements Stopper interface.
+func (s *StopByConfirmedBlocks) ShouldStop(vID types.ValidatorID) bool {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ // Accumulate confirmed blocks proposed by this validator in this round.
+ lastChecked := s.lastCheckDelivered[vID]
+ currentConfirmedBlocks := s.confirmedBlocks[vID]
+ db := s.dbs[vID]
+ s.apps[vID].Check(func(app *App) {
+ for _, h := range app.DeliverSequence[lastChecked:] {
+ b, err := db.Get(h)
+ if err != nil {
+ panic(err)
+ }
+ if b.ProposerID == vID {
+ currentConfirmedBlocks++
+ }
+ }
+ s.lastCheckDelivered[vID] = len(app.DeliverSequence)
+ })
+ s.confirmedBlocks[vID] = currentConfirmedBlocks
+ // Check if all validators confirmed at least 'blockCount' blocks.
+ for _, v := range s.confirmedBlocks {
+ if v < s.blockCount {
+ return false
+ }
+ }
+ return true
+}
diff --git a/core/test/stopper_test.go b/core/test/stopper_test.go
new file mode 100644
index 0000000..2abd503
--- /dev/null
+++ b/core/test/stopper_test.go
@@ -0,0 +1,103 @@
+// 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 test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/dexon-foundation/dexon-consensus-core/blockdb"
+ "github.com/dexon-foundation/dexon-consensus-core/common"
+ "github.com/dexon-foundation/dexon-consensus-core/core/types"
+ "github.com/stretchr/testify/suite"
+)
+
+type StopperTestSuite struct {
+ suite.Suite
+}
+
+func (s *StopperTestSuite) TestStopByConfirmedBlocks() {
+ // This test case makes sure this stopper would stop when
+ // all validators confirmed at least 'x' count of blocks produced
+ // by themselves.
+ var (
+ req = s.Require()
+ )
+
+ apps := make(map[types.ValidatorID]*App)
+ dbs := make(map[types.ValidatorID]blockdb.BlockDatabase)
+ validators := GenerateRandomValidatorIDs(2)
+ db, err := blockdb.NewMemBackedBlockDB()
+ req.Nil(err)
+ for _, vID := range validators {
+ apps[vID] = NewApp()
+ dbs[vID] = db
+ }
+ deliver := func(blocks []*types.Block) {
+ hashes := common.Hashes{}
+ for _, b := range blocks {
+ hashes = append(hashes, b.Hash)
+ req.Nil(db.Put(*b))
+ }
+ for _, vID := range validators {
+ app := apps[vID]
+ for _, h := range hashes {
+ app.StronglyAcked(h)
+ }
+ app.TotalOrderingDeliver(hashes, false)
+ for _, h := range hashes {
+ app.DeliverBlock(h, time.Time{})
+ }
+ }
+ }
+ stopper := NewStopByConfirmedBlocks(2, apps, dbs)
+ b00 := &types.Block{
+ ProposerID: validators[0],
+ Hash: common.NewRandomHash(),
+ }
+ deliver([]*types.Block{b00})
+ b10 := &types.Block{
+ ProposerID: validators[1],
+ Hash: common.NewRandomHash(),
+ }
+ b11 := &types.Block{
+ ProposerID: validators[1],
+ ParentHash: b10.Hash,
+ Hash: common.NewRandomHash(),
+ }
+ deliver([]*types.Block{b10, b11})
+ req.False(stopper.ShouldStop(validators[1]))
+ b12 := &types.Block{
+ ProposerID: validators[1],
+ ParentHash: b11.Hash,
+ Hash: common.NewRandomHash(),
+ }
+ deliver([]*types.Block{b12})
+ req.False(stopper.ShouldStop(validators[1]))
+ b01 := &types.Block{
+ ProposerID: validators[0],
+ ParentHash: b00.Hash,
+ Hash: common.NewRandomHash(),
+ }
+ deliver([]*types.Block{b01})
+ req.True(stopper.ShouldStop(validators[0]))
+}
+
+func TestStopper(t *testing.T) {
+ suite.Run(t, new(StopperTestSuite))
+}