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