github.com/koko1123/flow-go-1@v0.29.6/engine/collection/epochmgr/engine_test.go (about)

     1  package epochmgr
     2  
     3  import (
     4  	"io"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/rs/zerolog"
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/suite"
    12  
    13  	"github.com/koko1123/flow-go-1/consensus/hotstuff"
    14  	mockhotstuff "github.com/koko1123/flow-go-1/consensus/hotstuff/mocks"
    15  	epochmgr "github.com/koko1123/flow-go-1/engine/collection/epochmgr/mock"
    16  	"github.com/koko1123/flow-go-1/model/flow"
    17  	realmodule "github.com/koko1123/flow-go-1/module"
    18  	"github.com/koko1123/flow-go-1/module/mempool"
    19  	"github.com/koko1123/flow-go-1/module/mempool/epochs"
    20  	"github.com/koko1123/flow-go-1/module/mempool/herocache"
    21  	"github.com/koko1123/flow-go-1/module/metrics"
    22  	module "github.com/koko1123/flow-go-1/module/mock"
    23  	"github.com/koko1123/flow-go-1/network"
    24  	"github.com/koko1123/flow-go-1/network/mocknetwork"
    25  	realcluster "github.com/koko1123/flow-go-1/state/cluster"
    26  	cluster "github.com/koko1123/flow-go-1/state/cluster/mock"
    27  	realprotocol "github.com/koko1123/flow-go-1/state/protocol"
    28  	events "github.com/koko1123/flow-go-1/state/protocol/events/mock"
    29  	protocol "github.com/koko1123/flow-go-1/state/protocol/mock"
    30  	"github.com/koko1123/flow-go-1/utils/unittest"
    31  	"github.com/koko1123/flow-go-1/utils/unittest/mocks"
    32  )
    33  
    34  // mockComponents is a container for the mocked version of epoch components.
    35  type mockComponents struct {
    36  	state      *cluster.State
    37  	prop       *mocknetwork.Engine
    38  	sync       *mocknetwork.Engine
    39  	hotstuff   *module.HotStuff
    40  	aggregator *mockhotstuff.VoteAggregator
    41  }
    42  
    43  func newMockComponents() *mockComponents {
    44  
    45  	components := &mockComponents{
    46  		state:      new(cluster.State),
    47  		prop:       new(mocknetwork.Engine),
    48  		sync:       new(mocknetwork.Engine),
    49  		hotstuff:   new(module.HotStuff),
    50  		aggregator: new(mockhotstuff.VoteAggregator),
    51  	}
    52  	unittest.ReadyDoneify(components.prop)
    53  	unittest.ReadyDoneify(components.sync)
    54  	unittest.ReadyDoneify(components.hotstuff)
    55  	unittest.ReadyDoneify(components.aggregator)
    56  
    57  	// for now only aggregator and hotstuff supports module.Startable, mock only it
    58  	components.hotstuff.On("Start", mock.Anything)
    59  	components.aggregator.On("Start", mock.Anything)
    60  
    61  	return components
    62  }
    63  
    64  type Suite struct {
    65  	suite.Suite
    66  
    67  	// engine dependencies
    68  	log   zerolog.Logger
    69  	me    *module.Local
    70  	state *protocol.State
    71  	snap  *protocol.Snapshot
    72  	pools *epochs.TransactionPools
    73  
    74  	// qc voter dependencies
    75  	signer  *mockhotstuff.Signer
    76  	client  *module.QCContractClient
    77  	voter   *module.ClusterRootQCVoter
    78  	factory *epochmgr.EpochComponentsFactory
    79  	heights *events.Heights
    80  
    81  	epochQuery *mocks.EpochQuery
    82  	counter    uint64                     // reflects the counter of the current epoch
    83  	epochs     map[uint64]*protocol.Epoch // track all epochs
    84  	components map[uint64]*mockComponents // track all epoch components
    85  
    86  	engine *Engine
    87  }
    88  
    89  func (suite *Suite) SetupTest() {
    90  
    91  	suite.log = zerolog.New(io.Discard)
    92  	suite.me = new(module.Local)
    93  	suite.state = new(protocol.State)
    94  	suite.snap = new(protocol.Snapshot)
    95  
    96  	suite.epochs = make(map[uint64]*protocol.Epoch)
    97  	suite.components = make(map[uint64]*mockComponents)
    98  
    99  	suite.signer = new(mockhotstuff.Signer)
   100  	suite.client = new(module.QCContractClient)
   101  	suite.voter = new(module.ClusterRootQCVoter)
   102  	suite.factory = new(epochmgr.EpochComponentsFactory)
   103  	suite.heights = new(events.Heights)
   104  
   105  	// mock out Create so that it instantiates the appropriate mocks
   106  	suite.factory.On("Create", mock.Anything).
   107  		Run(func(args mock.Arguments) {
   108  			epoch, ok := args.Get(0).(realprotocol.Epoch)
   109  			suite.Require().Truef(ok, "invalid type %T", args.Get(0))
   110  			counter, err := epoch.Counter()
   111  			suite.Require().Nil(err)
   112  			suite.components[counter] = newMockComponents()
   113  		}).
   114  		Return(
   115  			func(epoch realprotocol.Epoch) realcluster.State { return suite.ComponentsForEpoch(epoch).state },
   116  			func(epoch realprotocol.Epoch) network.Engine { return suite.ComponentsForEpoch(epoch).prop },
   117  			func(epoch realprotocol.Epoch) network.Engine { return suite.ComponentsForEpoch(epoch).sync },
   118  			func(epoch realprotocol.Epoch) realmodule.HotStuff { return suite.ComponentsForEpoch(epoch).hotstuff },
   119  			func(epoch realprotocol.Epoch) hotstuff.VoteAggregator {
   120  				return suite.ComponentsForEpoch(epoch).aggregator
   121  			},
   122  			func(epoch realprotocol.Epoch) error { return nil },
   123  		)
   124  
   125  	suite.epochQuery = mocks.NewEpochQuery(suite.T(), suite.counter)
   126  	suite.state.On("Final").Return(suite.snap)
   127  	suite.snap.On("Epochs").Return(suite.epochQuery)
   128  
   129  	// add current and next epochs
   130  	suite.AddEpoch(suite.counter)
   131  	suite.AddEpoch(suite.counter + 1)
   132  
   133  	suite.pools = epochs.NewTransactionPools(func(_ uint64) mempool.Transactions {
   134  		return herocache.NewTransactions(1000, suite.log, metrics.NewNoopCollector())
   135  	})
   136  
   137  	var err error
   138  	suite.engine, err = New(suite.log, suite.me, suite.state, suite.pools, suite.voter, suite.factory, suite.heights)
   139  	suite.Require().Nil(err)
   140  }
   141  
   142  func TestEpochManager(t *testing.T) {
   143  	suite.Run(t, new(Suite))
   144  }
   145  
   146  // TransitionEpoch triggers an epoch transition in the suite's mocks.
   147  func (suite *Suite) TransitionEpoch() {
   148  	suite.counter++
   149  	suite.epochQuery.Transition()
   150  }
   151  
   152  // AddEpoch adds an epoch with the given counter.
   153  func (suite *Suite) AddEpoch(counter uint64) *protocol.Epoch {
   154  	epoch := new(protocol.Epoch)
   155  	epoch.On("Counter").Return(counter, nil)
   156  	suite.epochs[counter] = epoch
   157  	suite.epochQuery.Add(epoch)
   158  	return epoch
   159  }
   160  
   161  // AssertEpochStarted asserts that the components for the given epoch have been started.
   162  func (suite *Suite) AssertEpochStarted(counter uint64) {
   163  	components, ok := suite.components[counter]
   164  	suite.Assert().True(ok, "asserting nonexistent epoch started", counter)
   165  	components.prop.AssertCalled(suite.T(), "Ready")
   166  	components.sync.AssertCalled(suite.T(), "Ready")
   167  	components.aggregator.AssertCalled(suite.T(), "Ready")
   168  	components.aggregator.AssertCalled(suite.T(), "Start", mock.Anything)
   169  }
   170  
   171  // AssertEpochStopped asserts that the components for the given epoch have been stopped.
   172  func (suite *Suite) AssertEpochStopped(counter uint64) {
   173  	components, ok := suite.components[counter]
   174  	suite.Assert().True(ok, "asserting nonexistent epoch stopped", counter)
   175  	components.prop.AssertCalled(suite.T(), "Done")
   176  	components.sync.AssertCalled(suite.T(), "Done")
   177  }
   178  
   179  func (suite *Suite) ComponentsForEpoch(epoch realprotocol.Epoch) *mockComponents {
   180  	counter, err := epoch.Counter()
   181  	suite.Require().Nil(err, "cannot get counter")
   182  	components, ok := suite.components[counter]
   183  	suite.Require().True(ok, "missing component for counter", counter)
   184  	return components
   185  }
   186  
   187  // MockAsUnauthorizedNode mocks the factory to return a sentinel indicating
   188  // we are not authorized in the epoch
   189  func (suite *Suite) MockAsUnauthorizedNode() {
   190  
   191  	suite.factory = new(epochmgr.EpochComponentsFactory)
   192  	suite.factory.
   193  		On("Create", mock.Anything).
   194  		Return(nil, nil, nil, nil, nil, ErrNotAuthorizedForEpoch)
   195  
   196  	var err error
   197  	suite.engine, err = New(suite.log, suite.me, suite.state, suite.pools, suite.voter, suite.factory, suite.heights)
   198  	suite.Require().Nil(err)
   199  }
   200  
   201  // if we start up during the setup phase, we should kick off the root QC voter
   202  func (suite *Suite) TestRestartInSetupPhase() {
   203  
   204  	suite.snap.On("Phase").Return(flow.EpochPhaseSetup, nil)
   205  	// should call voter with next epoch
   206  	var called = make(chan struct{})
   207  	suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()).
   208  		Return(nil).
   209  		Run(func(args mock.Arguments) {
   210  			close(called)
   211  		}).Once()
   212  
   213  	// start up the engine
   214  	unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second)
   215  	unittest.AssertClosesBefore(suite.T(), called, time.Second)
   216  
   217  	suite.voter.AssertExpectations(suite.T())
   218  }
   219  
   220  // When a collection node joins the network at an epoch boundary, they must
   221  // start running during the EpochSetup phase in the epoch before they become
   222  // an authorized member so they submit their cluster QC vote.
   223  //
   224  // These nodes must kick off the root QC voter but should not attempt to
   225  // participate in cluster consensus in the current epoch.
   226  func (suite *Suite) TestStartAsUnauthorizedNode() {
   227  	suite.MockAsUnauthorizedNode()
   228  
   229  	// we are in setup phase
   230  	suite.snap.On("Phase").Return(flow.EpochPhaseSetup, nil)
   231  
   232  	// should call voter with next epoch
   233  	var called = make(chan struct{})
   234  	suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()).
   235  		Return(nil).
   236  		Run(func(args mock.Arguments) {
   237  			close(called)
   238  		}).Once()
   239  
   240  	// start the engine
   241  	unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second)
   242  
   243  	// should have submitted vote
   244  	unittest.AssertClosesBefore(suite.T(), called, time.Second)
   245  	suite.voter.AssertExpectations(suite.T())
   246  	// should have no epoch components
   247  	assert.Empty(suite.T(), suite.engine.epochs, "should have 0 epoch components")
   248  }
   249  
   250  // should kick off root QC voter on setup phase start event
   251  func (suite *Suite) TestRespondToPhaseChange() {
   252  
   253  	// should call voter with next epoch
   254  	var called = make(chan struct{})
   255  	suite.voter.On("Vote", mock.Anything, suite.epochQuery.Next()).
   256  		Return(nil).
   257  		Run(func(args mock.Arguments) {
   258  			close(called)
   259  		}).Once()
   260  
   261  	first := unittest.BlockHeaderFixture()
   262  	suite.state.On("AtBlockID", first.ID()).Return(suite.snap)
   263  
   264  	suite.engine.EpochSetupPhaseStarted(0, first)
   265  	unittest.AssertClosesBefore(suite.T(), called, time.Second)
   266  
   267  	suite.voter.AssertExpectations(suite.T())
   268  }
   269  
   270  func (suite *Suite) TestRespondToEpochTransition() {
   271  
   272  	// we are in committed phase
   273  	suite.snap.On("Phase").Return(flow.EpochPhaseCommitted, nil)
   274  
   275  	// start the engine
   276  	unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second)
   277  
   278  	first := unittest.BlockHeaderFixture()
   279  	suite.state.On("AtBlockID", first.ID()).Return(suite.snap)
   280  
   281  	// should set up callback for height at which previous epoch expires
   282  	var expiryCallback func()
   283  	var done = make(chan struct{})
   284  	suite.heights.On("OnHeight", first.Height+flow.DefaultTransactionExpiry, mock.Anything).
   285  		Run(func(args mock.Arguments) {
   286  			expiryCallback = args.Get(1).(func())
   287  			close(done)
   288  		}).
   289  		Once()
   290  
   291  	// mock the epoch transition
   292  	suite.TransitionEpoch()
   293  	// notify the engine of the epoch transition
   294  	suite.engine.EpochTransition(suite.counter, first)
   295  	unittest.AssertClosesBefore(suite.T(), done, time.Second)
   296  	suite.Assert().NotNil(expiryCallback)
   297  
   298  	// the engine should have two epochs under management, the just ended epoch
   299  	// and the newly started epoch
   300  	suite.Assert().Len(suite.engine.epochs, 2)
   301  	_, exists := suite.engine.epochs[suite.counter-1]
   302  	suite.Assert().True(exists, "should have previous epoch components")
   303  	_, exists = suite.engine.epochs[suite.counter]
   304  	suite.Assert().True(exists, "should have current epoch components")
   305  
   306  	// the newly started (current) epoch should have been started
   307  	suite.AssertEpochStarted(suite.counter)
   308  
   309  	// when we invoke the callback registered to handle the previous epoch's
   310  	// expiry, the previous epoch components should be cleaned up
   311  	expiryCallback()
   312  
   313  	suite.Assert().Eventually(func() bool {
   314  		suite.engine.unit.Lock()
   315  		defer suite.engine.unit.Unlock()
   316  		return len(suite.engine.epochs) == 1
   317  	}, time.Second, time.Millisecond)
   318  
   319  	// after the previous epoch expires, we should only have current epoch
   320  	_, exists = suite.engine.epochs[suite.counter]
   321  	suite.Assert().True(exists, "should have current epoch components")
   322  	_, exists = suite.engine.epochs[suite.counter-1]
   323  	suite.Assert().False(exists, "should not have previous epoch components")
   324  
   325  	// the expired epoch should have been stopped
   326  	suite.AssertEpochStopped(suite.counter - 1)
   327  }