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 }