github.com/onflow/flow-go@v0.33.17/engine/common/follower/compliance_engine_test.go (about)

     1  package follower
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/stretchr/testify/suite"
    13  
    14  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    15  	followermock "github.com/onflow/flow-go/engine/common/follower/mock"
    16  	"github.com/onflow/flow-go/model/flow"
    17  	"github.com/onflow/flow-go/model/messages"
    18  	"github.com/onflow/flow-go/module/compliance"
    19  	"github.com/onflow/flow-go/module/irrecoverable"
    20  	"github.com/onflow/flow-go/module/metrics"
    21  	module "github.com/onflow/flow-go/module/mock"
    22  	"github.com/onflow/flow-go/network/channels"
    23  	"github.com/onflow/flow-go/network/mocknetwork"
    24  	storage "github.com/onflow/flow-go/storage/mock"
    25  	"github.com/onflow/flow-go/utils/unittest"
    26  )
    27  
    28  func TestFollowerEngine(t *testing.T) {
    29  	suite.Run(t, new(EngineSuite))
    30  }
    31  
    32  // EngineSuite wraps CoreSuite and stores additional state needed for ComplianceEngine specific logic.
    33  type EngineSuite struct {
    34  	suite.Suite
    35  
    36  	finalized *flow.Header
    37  	net       *mocknetwork.Network
    38  	con       *mocknetwork.Conduit
    39  	me        *module.Local
    40  	headers   *storage.Headers
    41  	core      *followermock.ComplianceCore
    42  
    43  	ctx    irrecoverable.SignalerContext
    44  	cancel context.CancelFunc
    45  	errs   <-chan error
    46  	engine *ComplianceEngine
    47  }
    48  
    49  func (s *EngineSuite) SetupTest() {
    50  
    51  	s.net = mocknetwork.NewNetwork(s.T())
    52  	s.con = mocknetwork.NewConduit(s.T())
    53  	s.me = module.NewLocal(s.T())
    54  	s.headers = storage.NewHeaders(s.T())
    55  
    56  	s.core = followermock.NewComplianceCore(s.T())
    57  	s.core.On("Start", mock.Anything).Return().Once()
    58  	unittest.ReadyDoneify(s.core)
    59  
    60  	nodeID := unittest.IdentifierFixture()
    61  	s.me.On("NodeID").Return(nodeID).Maybe()
    62  
    63  	s.net.On("Register", mock.Anything, mock.Anything).Return(s.con, nil)
    64  
    65  	metrics := metrics.NewNoopCollector()
    66  	s.finalized = unittest.BlockHeaderFixture()
    67  	eng, err := NewComplianceLayer(
    68  		unittest.Logger(),
    69  		s.net,
    70  		s.me,
    71  		metrics,
    72  		s.headers,
    73  		s.finalized,
    74  		s.core,
    75  		compliance.DefaultConfig())
    76  	require.Nil(s.T(), err)
    77  
    78  	s.engine = eng
    79  
    80  	s.ctx, s.cancel, s.errs = irrecoverable.WithSignallerAndCancel(context.Background())
    81  	s.engine.Start(s.ctx)
    82  	unittest.RequireCloseBefore(s.T(), s.engine.Ready(), time.Second, "engine failed to start")
    83  }
    84  
    85  // TearDownTest stops the engine and checks there are no errors thrown to the SignallerContext.
    86  func (s *EngineSuite) TearDownTest() {
    87  	s.cancel()
    88  	unittest.RequireCloseBefore(s.T(), s.engine.Done(), time.Second, "engine failed to stop")
    89  	select {
    90  	case err := <-s.errs:
    91  		assert.NoError(s.T(), err)
    92  	default:
    93  	}
    94  }
    95  
    96  // TestProcessSyncedBlock checks that processing single synced block results in call to FollowerCore.
    97  func (s *EngineSuite) TestProcessSyncedBlock() {
    98  	block := unittest.BlockWithParentFixture(s.finalized)
    99  
   100  	originID := unittest.IdentifierFixture()
   101  	done := make(chan struct{})
   102  	s.core.On("OnBlockRange", originID, []*flow.Block{block}).Return(nil).Run(func(_ mock.Arguments) {
   103  		close(done)
   104  	}).Once()
   105  
   106  	s.engine.OnSyncedBlocks(flow.Slashable[[]*messages.BlockProposal]{
   107  		OriginID: originID,
   108  		Message:  flowBlocksToBlockProposals(block),
   109  	})
   110  	unittest.AssertClosesBefore(s.T(), done, time.Second)
   111  }
   112  
   113  // TestProcessGossipedBlock check that processing single gossiped block results in call to FollowerCore.
   114  func (s *EngineSuite) TestProcessGossipedBlock() {
   115  	block := unittest.BlockWithParentFixture(s.finalized)
   116  
   117  	originID := unittest.IdentifierFixture()
   118  	done := make(chan struct{})
   119  	s.core.On("OnBlockRange", originID, []*flow.Block{block}).Return(nil).Run(func(_ mock.Arguments) {
   120  		close(done)
   121  	}).Once()
   122  
   123  	err := s.engine.Process(channels.ReceiveBlocks, originID, messages.NewBlockProposal(block))
   124  	require.NoError(s.T(), err)
   125  
   126  	unittest.AssertClosesBefore(s.T(), done, time.Second)
   127  }
   128  
   129  // TestProcessBlockFromComplianceInterface check that processing single gossiped block using compliance interface results in call to FollowerCore.
   130  func (s *EngineSuite) TestProcessBlockFromComplianceInterface() {
   131  	block := unittest.BlockWithParentFixture(s.finalized)
   132  
   133  	originID := unittest.IdentifierFixture()
   134  	done := make(chan struct{})
   135  	s.core.On("OnBlockRange", originID, []*flow.Block{block}).Return(nil).Run(func(_ mock.Arguments) {
   136  		close(done)
   137  	}).Once()
   138  
   139  	s.engine.OnBlockProposal(flow.Slashable[*messages.BlockProposal]{
   140  		OriginID: originID,
   141  		Message:  messages.NewBlockProposal(block),
   142  	})
   143  
   144  	unittest.AssertClosesBefore(s.T(), done, time.Second)
   145  }
   146  
   147  // TestProcessBatchOfDisconnectedBlocks tests that processing a batch that consists of one connected range and individual blocks
   148  // results in submitting all of them.
   149  func (s *EngineSuite) TestProcessBatchOfDisconnectedBlocks() {
   150  	originID := unittest.IdentifierFixture()
   151  	blocks := unittest.ChainFixtureFrom(10, s.finalized)
   152  	// drop second block
   153  	blocks = append(blocks[0:1], blocks[2:]...)
   154  	// drop second from end block
   155  	blocks = append(blocks[:len(blocks)-2], blocks[len(blocks)-1])
   156  
   157  	var wg sync.WaitGroup
   158  	wg.Add(3)
   159  	s.core.On("OnBlockRange", originID, blocks[0:1]).Run(func(_ mock.Arguments) {
   160  		wg.Done()
   161  	}).Return(nil).Once()
   162  	s.core.On("OnBlockRange", originID, blocks[1:len(blocks)-1]).Run(func(_ mock.Arguments) {
   163  		wg.Done()
   164  	}).Return(nil).Once()
   165  	s.core.On("OnBlockRange", originID, blocks[len(blocks)-1:]).Run(func(_ mock.Arguments) {
   166  		wg.Done()
   167  	}).Return(nil).Once()
   168  
   169  	s.engine.OnSyncedBlocks(flow.Slashable[[]*messages.BlockProposal]{
   170  		OriginID: originID,
   171  		Message:  flowBlocksToBlockProposals(blocks...),
   172  	})
   173  	unittest.RequireReturnsBefore(s.T(), wg.Wait, time.Millisecond*500, "expect to return before timeout")
   174  }
   175  
   176  // TestProcessFinalizedBlock tests processing finalized block results in updating last finalized view and propagating it to
   177  // FollowerCore.
   178  // After submitting new finalized block, we check if new batches are filtered based on new finalized view.
   179  func (s *EngineSuite) TestProcessFinalizedBlock() {
   180  	newFinalizedBlock := unittest.BlockHeaderWithParentFixture(s.finalized)
   181  
   182  	done := make(chan struct{})
   183  	s.core.On("OnFinalizedBlock", newFinalizedBlock).Run(func(_ mock.Arguments) {
   184  		close(done)
   185  	}).Return(nil).Once()
   186  	s.headers.On("ByBlockID", newFinalizedBlock.ID()).Return(newFinalizedBlock, nil).Once()
   187  
   188  	s.engine.OnFinalizedBlock(model.BlockFromFlow(newFinalizedBlock))
   189  	unittest.RequireCloseBefore(s.T(), done, time.Millisecond*500, "expect to close before timeout")
   190  
   191  	// check if batch gets filtered out since it's lower than finalized view
   192  	done = make(chan struct{})
   193  	block := unittest.BlockWithParentFixture(s.finalized)
   194  	block.Header.View = newFinalizedBlock.View - 1 // use block view lower than new latest finalized view
   195  
   196  	// use metrics mock to track that we have indeed processed the message, and the batch was filtered out since it was
   197  	// lower than finalized height
   198  	metricsMock := module.NewEngineMetrics(s.T())
   199  	metricsMock.On("MessageReceived", mock.Anything, metrics.MessageSyncedBlocks).Return().Once()
   200  	metricsMock.On("MessageHandled", mock.Anything, metrics.MessageSyncedBlocks).Run(func(_ mock.Arguments) {
   201  		close(done)
   202  	}).Return().Once()
   203  	s.engine.engMetrics = metricsMock
   204  
   205  	s.engine.OnSyncedBlocks(flow.Slashable[[]*messages.BlockProposal]{
   206  		OriginID: unittest.IdentifierFixture(),
   207  		Message:  flowBlocksToBlockProposals(block),
   208  	})
   209  	unittest.RequireCloseBefore(s.T(), done, time.Millisecond*500, "expect to close before timeout")
   210  	// check if message wasn't buffered in internal channel
   211  	select {
   212  	case <-s.engine.pendingConnectedBlocksChan:
   213  		s.Fail("channel has to be empty at this stage")
   214  	default:
   215  
   216  	}
   217  }
   218  
   219  // flowBlocksToBlockProposals is a helper function to transform types.
   220  func flowBlocksToBlockProposals(blocks ...*flow.Block) []*messages.BlockProposal {
   221  	result := make([]*messages.BlockProposal, 0, len(blocks))
   222  	for _, block := range blocks {
   223  		result = append(result, messages.NewBlockProposal(block))
   224  	}
   225  	return result
   226  }