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 }