github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/consensus/approvals/assignment_collector_tree_test.go (about) 1 package approvals_test 2 3 import ( 4 "errors" 5 "fmt" 6 "sync" 7 "testing" 8 9 mocktestify "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 "github.com/stretchr/testify/suite" 12 13 "github.com/onflow/flow-go/engine" 14 "github.com/onflow/flow-go/engine/consensus/approvals" 15 mockAC "github.com/onflow/flow-go/engine/consensus/approvals/mock" 16 "github.com/onflow/flow-go/model/flow" 17 "github.com/onflow/flow-go/utils/unittest" 18 ) 19 20 // AssignmentCollectorTreeSuite performs isolated testing of AssignmentCollectorTree. 21 // AssignmentCollectorTreeSuite has to correctly create collectors, cache them and maintain pruned state 22 // based on finalization and sealing events. 23 func TestAssignmentCollectorTree(t *testing.T) { 24 suite.Run(t, new(AssignmentCollectorTreeSuite)) 25 } 26 27 var factoryError = errors.New("factory error") 28 29 // mockedCollectorWrapper is a helper structure for holding mock.AssignmentCollector and ProcessingStatus 30 // this is needed for simplifying mocking of state transitions. 31 type mockedCollectorWrapper struct { 32 collector *mockAC.AssignmentCollector 33 status approvals.ProcessingStatus 34 } 35 36 type AssignmentCollectorTreeSuite struct { 37 approvals.BaseAssignmentCollectorTestSuite 38 39 collectorTree *approvals.AssignmentCollectorTree 40 factoryMethod approvals.NewCollectorFactoryMethod 41 mockedCollectors map[flow.Identifier]*mockedCollectorWrapper 42 } 43 44 func (s *AssignmentCollectorTreeSuite) SetupTest() { 45 s.BaseAssignmentCollectorTestSuite.SetupTest() 46 47 s.factoryMethod = func(result *flow.ExecutionResult) (approvals.AssignmentCollector, error) { 48 if wrapper, found := s.mockedCollectors[result.ID()]; found { 49 return wrapper.collector, nil 50 } 51 return nil, fmt.Errorf("mocked collector %v not found: %w", result.ID(), factoryError) 52 } 53 54 s.mockedCollectors = make(map[flow.Identifier]*mockedCollectorWrapper) 55 s.collectorTree = approvals.NewAssignmentCollectorTree(s.ParentBlock, s.Headers, s.factoryMethod) 56 57 s.prepareMockedCollector(s.IncorporatedResult.Result) 58 } 59 60 // prepareMockedCollector prepares a mocked collector and stores it in map, later it will be used 61 // to create new collector when factory method will be called 62 func (s *AssignmentCollectorTreeSuite) prepareMockedCollector(result *flow.ExecutionResult) *mockedCollectorWrapper { 63 collector := &mockAC.AssignmentCollector{} 64 collector.On("ResultID").Return(result.ID()).Maybe() 65 collector.On("Result").Return(result).Maybe() 66 collector.On("BlockID").Return(result.BlockID).Maybe() 67 collector.On("Block").Return(func() *flow.Header { 68 return s.Blocks[result.BlockID] 69 }).Maybe() 70 71 wrapper := &mockedCollectorWrapper{ 72 collector: collector, 73 status: approvals.CachingApprovals, 74 } 75 76 collector.On("ProcessingStatus").Return(func() approvals.ProcessingStatus { 77 return wrapper.status 78 }) 79 s.mockedCollectors[result.ID()] = wrapper 80 return wrapper 81 } 82 83 // requireStateTransition specifies that we are expecting the business logic to 84 // execute the specified state transition. 85 func requireStateTransition(wrapper *mockedCollectorWrapper, oldState, newState approvals.ProcessingStatus) { 86 fmt.Printf("Require state transition for %x %v -> %v\n", wrapper.collector.BlockID(), wrapper.status, newState) 87 wrapper.collector.On("ChangeProcessingStatus", 88 oldState, newState).Return(nil).Run(func(args mocktestify.Arguments) { 89 fmt.Printf("Performing state transition for %x %v -> %v\n", wrapper.collector.BlockID(), wrapper.status, newState) 90 wrapper.status = newState 91 }).Once() 92 } 93 94 // TestGetSize_ConcurrentAccess tests if assignment collector tree correctly returns size when concurrently adding 95 // items 96 func (s *AssignmentCollectorTreeSuite) TestGetSize_ConcurrentAccess() { 97 numberOfWorkers := 10 98 batchSize := 10 99 chain := unittest.ChainFixtureFrom(numberOfWorkers*batchSize, s.IncorporatedBlock) 100 result0 := unittest.ExecutionResultFixture() 101 receipts := unittest.ReceiptChainFor(chain, result0) 102 for _, block := range chain { 103 s.Blocks[block.ID()] = block.Header 104 } 105 for _, receipt := range receipts { 106 s.prepareMockedCollector(&receipt.ExecutionResult) 107 } 108 109 var wg sync.WaitGroup 110 wg.Add(numberOfWorkers) 111 for worker := 0; worker < numberOfWorkers; worker++ { 112 go func(workerIndex int) { 113 defer wg.Done() 114 for i := 0; i < batchSize; i++ { 115 result := &receipts[workerIndex*batchSize+i].ExecutionResult 116 collector, err := s.collectorTree.GetOrCreateCollector(result) 117 require.NoError(s.T(), err) 118 require.True(s.T(), collector.Created) 119 } 120 }(worker) 121 } 122 wg.Wait() 123 124 require.Equal(s.T(), uint64(len(receipts)), s.collectorTree.GetSize()) 125 } 126 127 // TestGetCollector tests basic case where previously created collector can be retrieved 128 func (s *AssignmentCollectorTreeSuite) TestGetCollector() { 129 result := unittest.ExecutionResultFixture(func(result *flow.ExecutionResult) { 130 result.BlockID = s.IncorporatedBlock.ID() 131 }) 132 s.prepareMockedCollector(result) 133 expectedCollector, err := s.collectorTree.GetOrCreateCollector(result) 134 require.NoError(s.T(), err) 135 require.True(s.T(), expectedCollector.Created) 136 collector := s.collectorTree.GetCollector(result.ID()) 137 require.Equal(s.T(), collector, expectedCollector.Collector) 138 139 // get collector for unknown result ID should return nil 140 collector = s.collectorTree.GetCollector(unittest.IdentifierFixture()) 141 require.Nil(s.T(), collector) 142 } 143 144 // TestGetCollectorsByInterval tests that GetCollectorsByInterval returns a slice 145 // with the AssignmentCollectors from the requested interval 146 func (s *AssignmentCollectorTreeSuite) TestGetCollectorsByInterval() { 147 chain := unittest.ChainFixtureFrom(10, s.ParentBlock) 148 receipts := unittest.ReceiptChainFor(chain, s.IncorporatedResult.Result) 149 for _, block := range chain { 150 s.Blocks[block.ID()] = block.Header 151 } 152 153 // Process all receipts except first one. This generates a chain of collectors but all of them will be 154 // in caching state, as the receipt connecting them to the sealed state has not been added yet. 155 // As `GetCollectorsByInterval` only returns verifying collectors, we expect an empty slice to be returned. 156 for index, receipt := range receipts { 157 result := &receipt.ExecutionResult 158 s.prepareMockedCollector(result) 159 if index > 0 { 160 createdCollector, err := s.collectorTree.GetOrCreateCollector(result) 161 require.NoError(s.T(), err) 162 require.True(s.T(), createdCollector.Created) 163 } 164 } 165 collectors := s.collectorTree.GetCollectorsByInterval(0, s.Block.Height+100) 166 require.Empty(s.T(), collectors) 167 168 // Now we add the connecting receipt. The AssignmentCollectorTree should then change the states 169 // of all added collectors from `CachingApprovals` to `VerifyingApprovals`. Therefore, we 170 // expect `GetCollectorsByInterval` to return all added collectors. 171 for _, receipt := range receipts { 172 mockedCollector := s.mockedCollectors[receipt.ExecutionResult.ID()] 173 requireStateTransition(mockedCollector, approvals.CachingApprovals, approvals.VerifyingApprovals) 174 } 175 _, err := s.collectorTree.GetOrCreateCollector(&receipts[0].ExecutionResult) 176 require.NoError(s.T(), err) 177 178 collectors = s.collectorTree.GetCollectorsByInterval(0, s.Block.Height+100) 179 require.Len(s.T(), collectors, len(receipts)) 180 181 for _, receipt := range receipts { 182 mockedCollector := s.mockedCollectors[receipt.ExecutionResult.ID()] 183 mockedCollector.collector.AssertExpectations(s.T()) 184 } 185 } 186 187 // TestGetOrCreateCollector tests that getting collector creates one on first call and returns from cache on second one. 188 func (s *AssignmentCollectorTreeSuite) TestGetOrCreateCollector_ReturnFromCache() { 189 result := unittest.ExecutionResultFixture(func(result *flow.ExecutionResult) { 190 result.BlockID = s.IncorporatedBlock.ID() 191 }) 192 s.prepareMockedCollector(result) 193 lazyCollector, err := s.collectorTree.GetOrCreateCollector(result) 194 require.NoError(s.T(), err) 195 require.True(s.T(), lazyCollector.Created) 196 require.Equal(s.T(), approvals.CachingApprovals, lazyCollector.Collector.ProcessingStatus()) 197 198 lazyCollector2, err := s.collectorTree.GetOrCreateCollector(result) 199 require.NoError(s.T(), err) 200 // should be returned from cache 201 require.False(s.T(), lazyCollector2.Created) 202 require.True(s.T(), lazyCollector.Collector == lazyCollector2.Collector) 203 } 204 205 // TestGetOrCreateCollector_FactoryError tests that AssignmentCollectorTree correctly handles factory method error 206 func (s *AssignmentCollectorTreeSuite) TestGetOrCreateCollector_FactoryError() { 207 result := unittest.ExecutionResultFixture() 208 lazyCollector, err := s.collectorTree.GetOrCreateCollector(result) 209 require.ErrorIs(s.T(), err, factoryError) 210 require.Nil(s.T(), lazyCollector) 211 } 212 213 // TestGetOrCreateCollector_CollectorParentIsSealed tests a case where we add a result for collector with sealed parent. 214 // In this specific case collector has to become verifying instead of caching. 215 func (s *AssignmentCollectorTreeSuite) TestGetOrCreateCollector_CollectorParentIsSealed() { 216 result := s.IncorporatedResult.Result 217 requireStateTransition(s.mockedCollectors[result.ID()], 218 approvals.CachingApprovals, approvals.VerifyingApprovals) 219 lazyCollector, err := s.collectorTree.GetOrCreateCollector(result) 220 require.NoError(s.T(), err) 221 require.True(s.T(), lazyCollector.Created) 222 require.Equal(s.T(), approvals.VerifyingApprovals, lazyCollector.Collector.ProcessingStatus()) 223 } 224 225 // TestGetOrCreateCollector_AddingSealedCollector tests a case when we are trying to add collector which is already sealed. 226 // Leveled forest doesn't accept vertexes lower than the lowest height. 227 func (s *AssignmentCollectorTreeSuite) TestGetOrCreateCollector_AddingSealedCollector() { 228 block := unittest.BlockWithParentFixture(s.ParentBlock) 229 s.Blocks[block.ID()] = block.Header 230 result := unittest.ExecutionResultFixture(unittest.WithBlock(block)) 231 s.prepareMockedCollector(result) 232 233 // generate a few sealed blocks 234 prevSealedBlock := block.Header 235 for i := 0; i < 5; i++ { 236 sealedBlock := unittest.BlockHeaderWithParentFixture(prevSealedBlock) 237 s.MarkFinalized(sealedBlock) 238 _ = s.collectorTree.FinalizeForkAtLevel(sealedBlock, sealedBlock) 239 } 240 241 // now adding a collector which is lower than sealed height should result in error 242 lazyCollector, err := s.collectorTree.GetOrCreateCollector(result) 243 require.Error(s.T(), err) 244 require.True(s.T(), engine.IsOutdatedInputError(err)) 245 require.Nil(s.T(), lazyCollector) 246 } 247 248 // TestFinalizeForkAtLevel_ProcessableAfterSealedParent tests scenario that finalized collector becomes processable 249 // after parent block gets sealed. More specifically this case: 250 // 251 // P <- A <- B[ER{A}] <- C[ER{B}] <- D[ER{C}] 252 // <- E[ER{A}] <- F[ER{E}] <- G[ER{F}] 253 // | 254 // finalized 255 // 256 // Initially P was executed, B is finalized and incorporates ER for A, C incorporates ER for B, D incorporates ER for C, 257 // E was forked from A and incorporates ER for A, but wasn't finalized, F incorporates ER for E, G incorporates ER for F 258 // Let's take a case where we have collectors for execution results ER{A}, ER{B}, ER{C}, ER{E}, ER{F}. 259 // All of those collectors are not processable because ER{A} doesn't have parent collector or is not a parent of sealed block. 260 // Test that when A becomes sealed {ER{B}, ER{C}} become processable 261 // but {ER{E}, ER{F}} are unprocessable since E wasn't part of finalized fork. 262 func (s *AssignmentCollectorTreeSuite) TestFinalizeForkAtLevel_ProcessableAfterSealedParent() { 263 s.IdentitiesCache[s.IncorporatedBlock.ID()] = s.AuthorizedVerifiers 264 // two forks 265 forks := make([][]*flow.Block, 2) 266 results := make([][]*flow.IncorporatedResult, 2) 267 268 firstResult := unittest.ExecutionResultFixture( 269 unittest.WithPreviousResult(*s.IncorporatedResult.Result), 270 unittest.WithExecutionResultBlockID(s.IncorporatedBlock.ID())) 271 s.prepareMockedCollector(firstResult) 272 for i := 0; i < len(forks); i++ { 273 fork := unittest.ChainFixtureFrom(3, s.IncorporatedBlock) 274 forks[i] = fork 275 prevResult := firstResult 276 // create execution results for all blocks except last one, since it won't be valid by definition 277 for _, block := range fork { 278 blockID := block.ID() 279 280 // update caches 281 s.Blocks[blockID] = block.Header 282 s.IdentitiesCache[blockID] = s.AuthorizedVerifiers 283 284 IR := unittest.IncorporatedResult.Fixture( 285 unittest.IncorporatedResult.WithResult(prevResult), 286 unittest.IncorporatedResult.WithIncorporatedBlockID(blockID)) 287 288 results[i] = append(results[i], IR) 289 290 collector, err := s.collectorTree.GetOrCreateCollector(IR.Result) 291 require.NoError(s.T(), err) 292 293 require.Equal(s.T(), approvals.CachingApprovals, collector.Collector.ProcessingStatus()) 294 295 // create execution result for previous block in chain 296 // this result will be incorporated in current block. 297 prevResult = unittest.ExecutionResultFixture( 298 unittest.WithPreviousResult(*prevResult), 299 unittest.WithExecutionResultBlockID(blockID), 300 ) 301 s.prepareMockedCollector(prevResult) 302 } 303 } 304 305 finalized := forks[0][0].Header 306 307 s.MarkFinalized(s.IncorporatedBlock) 308 s.MarkFinalized(finalized) 309 310 // at this point collectors for forks[0] should be processable and for forks[1] not 311 for forkIndex := range forks { 312 for resultIndex, result := range results[forkIndex] { 313 wrapper, found := s.mockedCollectors[result.Result.ID()] 314 fmt.Printf("forkIndex: %d, resultIndex: %d, id: %x, result: %v\n", forkIndex, resultIndex, result.Result.ID(), result.Result) 315 require.True(s.T(), found) 316 317 if forkIndex == 0 { 318 requireStateTransition(wrapper, approvals.CachingApprovals, approvals.VerifyingApprovals) 319 } else if resultIndex > 0 { 320 // first result shouldn't transfer to orphaned state since it's actually ER{A} which is part of finalized fork. 321 requireStateTransition(wrapper, approvals.CachingApprovals, approvals.Orphaned) 322 } 323 } 324 } 325 326 // A becomes sealed, B becomes finalized 327 err := s.collectorTree.FinalizeForkAtLevel(finalized, s.Block) 328 require.NoError(s.T(), err) 329 330 for forkIndex := range forks { 331 for _, result := range results[forkIndex] { 332 s.mockedCollectors[result.Result.ID()].collector.AssertExpectations(s.T()) 333 } 334 } 335 }