github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/consensus/approvals/verifying_assignment_collector_test.go (about)

     1  package approvals
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/gammazero/workerpool"
    10  	"github.com/rs/zerolog"
    11  	"github.com/stretchr/testify/mock"
    12  	"github.com/stretchr/testify/require"
    13  	"github.com/stretchr/testify/suite"
    14  
    15  	"github.com/onflow/crypto/hash"
    16  
    17  	"github.com/onflow/flow-go/engine"
    18  	"github.com/onflow/flow-go/engine/consensus/approvals/tracker"
    19  	"github.com/onflow/flow-go/model/chunks"
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/model/messages"
    22  	realmodule "github.com/onflow/flow-go/module"
    23  	realmempool "github.com/onflow/flow-go/module/mempool"
    24  	module "github.com/onflow/flow-go/module/mock"
    25  	"github.com/onflow/flow-go/network"
    26  	realproto "github.com/onflow/flow-go/state/protocol"
    27  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    28  	realstorage "github.com/onflow/flow-go/storage"
    29  	"github.com/onflow/flow-go/utils/unittest"
    30  )
    31  
    32  // TestAssignmentCollector tests behavior of AssignmentCollector in different scenarios
    33  // AssignmentCollector is responsible collecting approvals that satisfy one assignment, meaning that we will
    34  // have multiple collectorTree for one execution result as same result can be incorporated in multiple forks.
    35  // AssignmentCollector has a strict ordering of processing, before processing approvals at least one incorporated result has to be
    36  // processed.
    37  // AssignmentCollector takes advantage of internal caching to speed up processing approvals for different assignments
    38  // AssignmentCollector is responsible for validating approvals on result-level(checking signature, identity).
    39  func TestAssignmentCollector(t *testing.T) {
    40  	suite.Run(t, new(AssignmentCollectorTestSuite))
    41  }
    42  
    43  func newVerifyingAssignmentCollector(logger zerolog.Logger,
    44  	workerPool *workerpool.WorkerPool,
    45  	result *flow.ExecutionResult,
    46  	state realproto.State,
    47  	headers realstorage.Headers,
    48  	assigner realmodule.ChunkAssigner,
    49  	seals realmempool.IncorporatedResultSeals,
    50  	sigHasher hash.Hasher,
    51  	approvalConduit network.Conduit,
    52  	requestTracker *RequestTracker,
    53  	requiredApprovalsForSealConstruction uint,
    54  ) (*VerifyingAssignmentCollector, error) {
    55  	b, err := NewAssignmentCollectorBase(logger, workerPool, result, state, headers, assigner, seals, sigHasher,
    56  		approvalConduit, requestTracker, requiredApprovalsForSealConstruction)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	return NewVerifyingAssignmentCollector(b)
    61  }
    62  
    63  type AssignmentCollectorTestSuite struct {
    64  	BaseAssignmentCollectorTestSuite
    65  	collector *VerifyingAssignmentCollector
    66  }
    67  
    68  func (s *AssignmentCollectorTestSuite) SetupTest() {
    69  	s.BaseAssignmentCollectorTestSuite.SetupTest()
    70  
    71  	var err error
    72  	s.collector, err = newVerifyingAssignmentCollector(unittest.Logger(), s.WorkerPool, s.IncorporatedResult.Result, s.State, s.Headers,
    73  		s.Assigner, s.SealsPL, s.SigHasher, s.Conduit, s.RequestTracker, uint(len(s.AuthorizedVerifiers)))
    74  	require.NoError(s.T(), err)
    75  }
    76  
    77  // TestProcessApproval_ApprovalsAfterResult tests a scenario when first we have discovered execution result
    78  // and after that we started receiving approvals. In this scenario we should be able to create a seal right
    79  // after processing last needed approval to meet `requiredApprovalsForSealConstruction` threshold.
    80  func (s *AssignmentCollectorTestSuite) TestProcessApproval_ApprovalsAfterResult() {
    81  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
    82  	require.NoError(s.T(), err)
    83  
    84  	s.SealsPL.On("Add", mock.Anything).Run(
    85  		func(args mock.Arguments) {
    86  			seal := args.Get(0).(*flow.IncorporatedResultSeal)
    87  			require.Equal(s.T(), s.Block.ID(), seal.Seal.BlockID)
    88  			require.Equal(s.T(), s.IncorporatedResult.Result.ID(), seal.Seal.ResultID)
    89  		},
    90  	).Return(true, nil).Once()
    91  	s.PublicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil)
    92  
    93  	blockID := s.Block.ID()
    94  	resultID := s.IncorporatedResult.Result.ID()
    95  	for _, chunk := range s.Chunks {
    96  		for verID := range s.AuthorizedVerifiers {
    97  			approval := unittest.ResultApprovalFixture(unittest.WithChunk(chunk.Index),
    98  				unittest.WithApproverID(verID),
    99  				unittest.WithBlockID(blockID),
   100  				unittest.WithExecutionResultID(resultID))
   101  			err = s.collector.ProcessApproval(approval)
   102  			require.NoError(s.T(), err)
   103  		}
   104  	}
   105  
   106  	s.SealsPL.AssertExpectations(s.T())
   107  }
   108  
   109  // TestProcessIncorporatedResult_ReusingCachedApprovals tests a scenario where we successfully processed approvals for one incorporated result
   110  // and we are able to reuse those approvals for another incorporated result of same execution result
   111  func (s *AssignmentCollectorTestSuite) TestProcessIncorporatedResult_ReusingCachedApprovals() {
   112  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   113  	require.NoError(s.T(), err)
   114  
   115  	s.SealsPL.On("Add", mock.Anything).Return(true, nil).Twice()
   116  	s.PublicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil)
   117  
   118  	blockID := s.Block.ID()
   119  	resultID := s.IncorporatedResult.Result.ID()
   120  	for _, chunk := range s.Chunks {
   121  		for verID := range s.AuthorizedVerifiers {
   122  			approval := unittest.ResultApprovalFixture(unittest.WithChunk(chunk.Index),
   123  				unittest.WithApproverID(verID),
   124  				unittest.WithBlockID(blockID),
   125  				unittest.WithExecutionResultID(resultID))
   126  			err = s.collector.ProcessApproval(approval)
   127  			require.NoError(s.T(), err)
   128  		}
   129  	}
   130  
   131  	incorporatedBlock := unittest.BlockHeaderWithParentFixture(s.Block)
   132  	s.Blocks[incorporatedBlock.ID()] = incorporatedBlock
   133  
   134  	// at this point we have proposed a seal, let's construct new incorporated result with same assignment
   135  	// but different incorporated block ID resulting in new seal.
   136  	incorporatedResult := unittest.IncorporatedResult.Fixture(
   137  		unittest.IncorporatedResult.WithIncorporatedBlockID(incorporatedBlock.ID()),
   138  		unittest.IncorporatedResult.WithResult(s.IncorporatedResult.Result),
   139  	)
   140  
   141  	err = s.collector.ProcessIncorporatedResult(incorporatedResult)
   142  	require.NoError(s.T(), err)
   143  	s.SealsPL.AssertExpectations(s.T())
   144  
   145  }
   146  
   147  // TestProcessApproval_InvalidSignature tests a scenario processing approval with invalid signature
   148  func (s *AssignmentCollectorTestSuite) TestProcessApproval_InvalidSignature() {
   149  
   150  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   151  	require.NoError(s.T(), err)
   152  
   153  	approval := unittest.ResultApprovalFixture(unittest.WithChunk(s.Chunks[0].Index),
   154  		unittest.WithApproverID(s.VerID),
   155  		unittest.WithExecutionResultID(s.IncorporatedResult.Result.ID()))
   156  
   157  	// attestation signature is valid
   158  	s.PublicKey.On("Verify", mock.Anything, approval.Body.AttestationSignature, mock.Anything).Return(true, nil).Once()
   159  	// approval signature is invalid
   160  	s.PublicKey.On("Verify", mock.Anything, approval.VerifierSignature, mock.Anything).Return(false, nil).Once()
   161  
   162  	err = s.collector.ProcessApproval(approval)
   163  	require.Error(s.T(), err)
   164  	require.True(s.T(), engine.IsInvalidInputError(err))
   165  }
   166  
   167  // TestProcessApproval_InvalidBlockID tests a scenario processing approval with invalid block ID
   168  func (s *AssignmentCollectorTestSuite) TestProcessApproval_InvalidBlockID() {
   169  
   170  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   171  	require.NoError(s.T(), err)
   172  
   173  	approval := unittest.ResultApprovalFixture(unittest.WithChunk(s.Chunks[0].Index),
   174  		unittest.WithApproverID(s.VerID),
   175  		unittest.WithExecutionResultID(s.IncorporatedResult.Result.ID()))
   176  
   177  	err = s.collector.ProcessApproval(approval)
   178  	require.Error(s.T(), err)
   179  	require.True(s.T(), engine.IsInvalidInputError(err))
   180  }
   181  
   182  // TestProcessApproval_InvalidBlockChunkIndex tests a scenario processing approval with invalid chunk index
   183  func (s *AssignmentCollectorTestSuite) TestProcessApproval_InvalidBlockChunkIndex() {
   184  
   185  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   186  	require.NoError(s.T(), err)
   187  
   188  	approval := unittest.ResultApprovalFixture(unittest.WithChunk(uint64(s.Chunks.Len())),
   189  		unittest.WithApproverID(s.VerID),
   190  		unittest.WithExecutionResultID(s.IncorporatedResult.Result.ID()))
   191  
   192  	err = s.collector.ProcessApproval(approval)
   193  	require.Error(s.T(), err)
   194  	require.True(s.T(), engine.IsInvalidInputError(err))
   195  }
   196  
   197  // TestProcessIncorporatedResult tests different scenarios for processing incorporated result
   198  // Expected to process valid incorporated result without error and reject invalid incorporated results
   199  // with engine.InvalidInputError
   200  func (s *AssignmentCollectorTestSuite) TestProcessIncorporatedResult() {
   201  	s.Run("valid-incorporated-result", func() {
   202  		err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   203  		require.NoError(s.T(), err)
   204  	})
   205  
   206  	s.Run("invalid-assignment", func() {
   207  		assigner := &module.ChunkAssigner{}
   208  		assigner.On("Assign", mock.Anything, mock.Anything).Return(nil, fmt.Errorf(""))
   209  
   210  		collector, err := newVerifyingAssignmentCollector(unittest.Logger(), s.WorkerPool, s.IncorporatedResult.Result, s.State, s.Headers,
   211  			assigner, s.SealsPL, s.SigHasher, s.Conduit, s.RequestTracker, 1)
   212  		require.NoError(s.T(), err)
   213  
   214  		err = collector.ProcessIncorporatedResult(s.IncorporatedResult)
   215  		require.Error(s.T(), err)
   216  	})
   217  
   218  	s.Run("invalid-verifier-identities", func() {
   219  		// delete identities for Result.BlockID
   220  		delete(s.IdentitiesCache, s.IncorporatedResult.Result.BlockID)
   221  		s.Snapshots[s.IncorporatedResult.Result.BlockID] = unittest.StateSnapshotForKnownBlock(s.Block, nil)
   222  		collector, err := newVerifyingAssignmentCollector(unittest.Logger(), s.WorkerPool, s.IncorporatedResult.Result, s.State, s.Headers,
   223  			s.Assigner, s.SealsPL, s.SigHasher, s.Conduit, s.RequestTracker, 1)
   224  		require.Error(s.T(), err)
   225  		require.Nil(s.T(), collector)
   226  	})
   227  }
   228  
   229  // TestProcessIncorporatedResult_InvalidIdentity tests a few scenarios where verifier identity is not correct
   230  // by one or another reason
   231  func (s *AssignmentCollectorTestSuite) TestProcessIncorporatedResult_InvalidIdentity() {
   232  	// mocks state to return invalid identity and creates assignment collector that will use it
   233  	// creating assignment collector with invalid identity should result in error
   234  	assertInvalidIdentity := func(identity *flow.Identity) {
   235  		state := protocol.NewState(s.T())
   236  		state.On("AtBlockID", mock.Anything).Return(
   237  			func(blockID flow.Identifier) realproto.Snapshot {
   238  				return unittest.StateSnapshotForKnownBlock(
   239  					s.Block,
   240  					map[flow.Identifier]*flow.Identity{identity.NodeID: identity},
   241  				)
   242  			},
   243  		)
   244  
   245  		collector, err := newVerifyingAssignmentCollector(unittest.Logger(), s.WorkerPool, s.IncorporatedResult.Result, state, s.Headers, s.Assigner, s.SealsPL,
   246  			s.SigHasher, s.Conduit, s.RequestTracker, 1)
   247  		require.Error(s.T(), err)
   248  		require.Nil(s.T(), collector)
   249  	}
   250  
   251  	s.Run("verifier-zero-weight", func() {
   252  		identity := unittest.IdentityFixture(
   253  			unittest.WithRole(flow.RoleVerification),
   254  			unittest.WithParticipationStatus(flow.EpochParticipationStatusActive),
   255  			unittest.WithInitialWeight(0),
   256  		)
   257  		assertInvalidIdentity(identity)
   258  	})
   259  	s.Run("verifier-leaving", func() {
   260  		identity := unittest.IdentityFixture(
   261  			unittest.WithRole(flow.RoleVerification),
   262  			unittest.WithParticipationStatus(flow.EpochParticipationStatusLeaving),
   263  		)
   264  		assertInvalidIdentity(identity)
   265  	})
   266  	s.Run("verifier-joining", func() {
   267  		identity := unittest.IdentityFixture(
   268  			unittest.WithRole(flow.RoleVerification),
   269  			unittest.WithParticipationStatus(flow.EpochParticipationStatusJoining),
   270  		)
   271  		assertInvalidIdentity(identity)
   272  	})
   273  	s.Run("verifier-ejected", func() {
   274  		identity := unittest.IdentityFixture(
   275  			unittest.WithRole(flow.RoleVerification),
   276  			unittest.WithParticipationStatus(flow.EpochParticipationStatusEjected),
   277  		)
   278  		assertInvalidIdentity(identity)
   279  	})
   280  	s.Run("verifier-invalid-role", func() {
   281  		// invalid role
   282  		identity := unittest.IdentityFixture(unittest.WithRole(flow.RoleAccess))
   283  		assertInvalidIdentity(identity)
   284  	})
   285  }
   286  
   287  // TestProcessApproval_BeforeIncorporatedResult tests scenario when approval is submitted before execution result
   288  // is discovered, without execution result we are missing information for verification. Calling `ProcessApproval` before `ProcessApproval`
   289  // should result in error
   290  func (s *AssignmentCollectorTestSuite) TestProcessApproval_BeforeIncorporatedResult() {
   291  	approval := unittest.ResultApprovalFixture(unittest.WithChunk(s.Chunks[0].Index),
   292  		unittest.WithApproverID(s.VerID),
   293  		unittest.WithExecutionResultID(s.IncorporatedResult.Result.ID()))
   294  	err := s.collector.ProcessApproval(approval)
   295  	require.Error(s.T(), err)
   296  	require.True(s.T(), engine.IsInvalidInputError(err))
   297  }
   298  
   299  // TestRequestMissingApprovals checks that requests are sent only for chunks
   300  // that have not collected enough approvals yet, and are sent only to the
   301  // verifiers assigned to those chunks. It also checks that the threshold and
   302  // rate limiting is respected.
   303  func (s *AssignmentCollectorTestSuite) TestRequestMissingApprovals() {
   304  	// build new assignment with 2 verifiers
   305  	assignment := chunks.NewAssignment()
   306  	for _, chunk := range s.Chunks {
   307  		verifiers := s.ChunksAssignment.Verifiers(chunk)
   308  		assignment.Add(chunk, verifiers[:2])
   309  	}
   310  	// replace old one
   311  	s.ChunksAssignment = assignment
   312  
   313  	incorporatedBlocks := make([]*flow.Header, 0)
   314  
   315  	lastHeight := uint64(rand.Uint32())
   316  	for i := 0; i < 2; i++ {
   317  		incorporatedBlock := unittest.BlockHeaderFixture()
   318  		incorporatedBlock.Height = lastHeight
   319  		lastHeight++
   320  
   321  		s.Blocks[incorporatedBlock.ID()] = incorporatedBlock
   322  		incorporatedBlocks = append(incorporatedBlocks, incorporatedBlock)
   323  	}
   324  
   325  	incorporatedResults := make([]*flow.IncorporatedResult, 0, len(incorporatedBlocks))
   326  	for _, block := range incorporatedBlocks {
   327  		incorporatedResult := unittest.IncorporatedResult.Fixture(
   328  			unittest.IncorporatedResult.WithResult(s.IncorporatedResult.Result),
   329  			unittest.IncorporatedResult.WithIncorporatedBlockID(block.ID()))
   330  		incorporatedResults = append(incorporatedResults, incorporatedResult)
   331  
   332  		err := s.collector.ProcessIncorporatedResult(incorporatedResult)
   333  		require.NoError(s.T(), err)
   334  	}
   335  
   336  	requests := make([]*messages.ApprovalRequest, 0)
   337  	// mock the Publish method when requests are sent to 2 verifiers
   338  	s.Conduit.On("Publish", mock.Anything, mock.Anything, mock.Anything).
   339  		Return(nil).
   340  		Run(func(args mock.Arguments) {
   341  			// collect the request
   342  			ar, ok := args[0].(*messages.ApprovalRequest)
   343  			s.Assert().True(ok)
   344  			requests = append(requests, ar)
   345  		})
   346  
   347  	requestCount, err := s.collector.RequestMissingApprovals(&tracker.NoopSealingTracker{}, lastHeight)
   348  	require.NoError(s.T(), err)
   349  
   350  	// first time it goes through, no requests should be made because of the
   351  	// blackout period
   352  	require.Len(s.T(), requests, 0)
   353  	require.Zero(s.T(), requestCount)
   354  
   355  	// wait for the max blackout period to elapse and retry
   356  	time.Sleep(3 * time.Second)
   357  
   358  	// requesting with immature height will be ignored
   359  	requestCount, err = s.collector.RequestMissingApprovals(&tracker.NoopSealingTracker{}, lastHeight-uint64(len(incorporatedBlocks))-1)
   360  	s.Require().NoError(err)
   361  	require.Len(s.T(), requests, 0)
   362  	require.Zero(s.T(), requestCount)
   363  
   364  	requestCount, err = s.collector.RequestMissingApprovals(&tracker.NoopSealingTracker{}, lastHeight)
   365  	s.Require().NoError(err)
   366  
   367  	require.Equal(s.T(), int(requestCount), s.Chunks.Len()*len(s.collector.collectors))
   368  	require.Len(s.T(), requests, s.Chunks.Len()*len(s.collector.collectors))
   369  
   370  	result := s.IncorporatedResult.Result
   371  	for _, chunk := range s.Chunks {
   372  		for _, incorporatedResult := range incorporatedResults {
   373  			requestItem, _, err := s.RequestTracker.TryUpdate(result, incorporatedResult.IncorporatedBlockID, chunk.Index)
   374  			require.NoError(s.T(), err)
   375  			require.Equal(s.T(), uint(1), requestItem.Requests)
   376  		}
   377  	}
   378  }
   379  
   380  // TestCheckEmergencySealing tests that currently tracked incorporated results can be emergency sealed
   381  // when height difference reached the emergency sealing threshold.
   382  func (s *AssignmentCollectorTestSuite) TestCheckEmergencySealing() {
   383  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   384  	require.NoError(s.T(), err)
   385  
   386  	// checking emergency sealing with current height
   387  	// should early exit without creating any seals
   388  	err = s.collector.CheckEmergencySealing(&tracker.NoopSealingTracker{}, s.IncorporatedBlock.Height)
   389  	require.NoError(s.T(), err)
   390  
   391  	s.SealsPL.On("Add", mock.Anything).Run(
   392  		func(args mock.Arguments) {
   393  			seal := args.Get(0).(*flow.IncorporatedResultSeal)
   394  			require.Equal(s.T(), s.Block.ID(), seal.Seal.BlockID)
   395  			require.Equal(s.T(), s.IncorporatedResult.Result.ID(), seal.Seal.ResultID)
   396  		},
   397  	).Return(true, nil).Once()
   398  
   399  	err = s.collector.CheckEmergencySealing(&tracker.NoopSealingTracker{}, DefaultEmergencySealingThresholdForFinalization+s.IncorporatedBlock.Height)
   400  	require.NoError(s.T(), err)
   401  
   402  	s.SealsPL.AssertExpectations(s.T())
   403  }
   404  
   405  // test that when
   406  func (s *AssignmentCollectorTestSuite) TestCheckEmergencySealingNotEnoughFinalizedBlocks() {
   407  	err := s.collector.ProcessIncorporatedResult(s.IncorporatedResult)
   408  	require.NoError(s.T(), err)
   409  
   410  	err = s.collector.CheckEmergencySealing(&tracker.NoopSealingTracker{}, DefaultEmergencySealingThresholdForVerification+s.IncorporatedBlock.Height)
   411  	require.NoError(s.T(), err)
   412  
   413  	// SealsPL.Add is not being called, because there isn't enough finalized blocks to trigger
   414  	// emergency sealing
   415  	s.SealsPL.AssertExpectations(s.T())
   416  }