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 }