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  }