github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/validation/receipt_validator_test.go (about) 1 package validation 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/mock" 7 "github.com/stretchr/testify/require" 8 "github.com/stretchr/testify/suite" 9 10 "github.com/onflow/flow-go/engine" 11 "github.com/onflow/flow-go/model/flow" 12 "github.com/onflow/flow-go/module" 13 fmock "github.com/onflow/flow-go/module/mock" 14 "github.com/onflow/flow-go/utils/unittest" 15 ) 16 17 func TestReceiptValidator(t *testing.T) { 18 suite.Run(t, new(ReceiptValidationSuite)) 19 } 20 21 type ReceiptValidationSuite struct { 22 unittest.BaseChainSuite 23 24 receiptValidator module.ReceiptValidator 25 publicKey *fmock.PublicKey 26 } 27 28 func (s *ReceiptValidationSuite) SetupTest() { 29 s.SetupChain() 30 s.publicKey = &fmock.PublicKey{} 31 s.Identities[s.ExeID].StakingPubKey = s.publicKey 32 s.receiptValidator = NewReceiptValidator( 33 s.State, 34 s.HeadersDB, 35 s.IndexDB, 36 s.ResultsDB, 37 s.SealsDB, 38 ) 39 } 40 41 // TestReceiptValid try submitting valid receipt 42 func (s *ReceiptValidationSuite) TestReceiptValid() { 43 valSubgrph := s.ValidSubgraphFixture() 44 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 45 unittest.WithResult(valSubgrph.Result)) 46 s.AddSubgraphFixtureToMempools(valSubgrph) 47 48 receiptID := receipt.ID() 49 s.publicKey.On("Verify", 50 receipt.ExecutorSignature, 51 receiptID[:], 52 mock.Anything, 53 ).Return(true, nil).Once() 54 55 err := s.receiptValidator.Validate(receipt) 56 s.Require().NoError(err, "should successfully validate receipt") 57 s.publicKey.AssertExpectations(s.T()) 58 } 59 60 // TestReceiptNoIdentity tests that we reject receipt with invalid `ExecutionResult.ExecutorID` 61 func (s *ReceiptValidationSuite) TestReceiptNoIdentity() { 62 valSubgrph := s.ValidSubgraphFixture() 63 node := unittest.IdentityFixture() 64 mockPk := &fmock.PublicKey{} 65 node.StakingPubKey = mockPk 66 67 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(node.NodeID), 68 unittest.WithResult(valSubgrph.Result)) 69 s.AddSubgraphFixtureToMempools(valSubgrph) 70 receiptID := receipt.ID() 71 72 mockPk.On("Verify", 73 receiptID[:], 74 receipt.ExecutorSignature, 75 mock.Anything, 76 ).Return(true, nil).Once() 77 err := s.receiptValidator.Validate(receipt) 78 s.Require().Error(err, "should reject invalid identity") 79 s.Assert().True(engine.IsInvalidInputError(err)) 80 } 81 82 // TestReceiptFromNonActiveNode tests that we reject receipt from an execution node which is not authorized to participate: 83 // - execution node is joining 84 // - execution node is leaving 85 // - execution node has zero initial weight. 86 func (s *ReceiptValidationSuite) TestReceiptFromNonAuthorizedNode() { 87 valSubgrph := s.ValidSubgraphFixture() 88 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 89 unittest.WithResult(valSubgrph.Result)) 90 s.AddSubgraphFixtureToMempools(valSubgrph) 91 92 s.publicKey.On("Verify", 93 mock.Anything, 94 mock.Anything, 95 mock.Anything).Return(true, nil).Maybe() // call optional, as validator might check weight first 96 97 s.Run("execution-node-leaving", func() { 98 // replace EN participation status 99 s.Identities[s.ExeID].EpochParticipationStatus = flow.EpochParticipationStatusLeaving 100 101 err := s.receiptValidator.Validate(receipt) 102 s.Require().Error(err, "should reject invalid weight") 103 s.Assert().True(engine.IsInvalidInputError(err)) 104 }) 105 s.Run("execution-node-joining", func() { 106 // replace EN participation status 107 s.Identities[s.ExeID].EpochParticipationStatus = flow.EpochParticipationStatusJoining 108 109 err := s.receiptValidator.Validate(receipt) 110 s.Require().Error(err, "should reject invalid weight") 111 s.Assert().True(engine.IsInvalidInputError(err)) 112 }) 113 s.Run("execution-node-zero-weight", func() { 114 // replace EN participation status and initial weight 115 s.Identities[s.ExeID].EpochParticipationStatus = flow.EpochParticipationStatusActive 116 s.Identities[s.ExeID].InitialWeight = 0 117 118 err := s.receiptValidator.Validate(receipt) 119 s.Require().Error(err, "should reject invalid weight") 120 s.Assert().True(engine.IsInvalidInputError(err)) 121 }) 122 } 123 124 // TestReceiptInvalidRole tests that we reject receipt with invalid execution node role 125 func (s *ReceiptValidationSuite) TestReceiptInvalidRole() { 126 valSubgrph := s.ValidSubgraphFixture() 127 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 128 unittest.WithResult(valSubgrph.Result)) 129 s.AddSubgraphFixtureToMempools(valSubgrph) 130 131 s.publicKey.On("Verify", 132 mock.Anything, 133 mock.Anything, 134 mock.Anything).Return(true, nil).Maybe() // call optional, as validator might check weight first 135 136 // replace identity with invalid one 137 s.Identities[s.ExeID] = unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)) 138 139 err := s.receiptValidator.Validate(receipt) 140 s.Require().Error(err, "should reject invalid identity") 141 s.Assert().True(engine.IsInvalidInputError(err)) 142 } 143 144 // TestReceiptInvalidSignature tests that we reject receipt with invalid signature 145 func (s *ReceiptValidationSuite) TestReceiptInvalidSignature() { 146 147 valSubgrph := s.ValidSubgraphFixture() 148 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 149 unittest.WithResult(valSubgrph.Result)) 150 s.AddSubgraphFixtureToMempools(valSubgrph) 151 152 s.publicKey.On("Verify", 153 mock.Anything, 154 mock.Anything, 155 mock.Anything, 156 ).Return(false, nil).Once() 157 158 err := s.receiptValidator.Validate(receipt) 159 s.Require().Error(err, "should reject invalid signature") 160 s.Assert().True(engine.IsInvalidInputError(err)) 161 s.publicKey.AssertExpectations(s.T()) 162 } 163 164 // TestReceiptTooFewChunks tests that we reject receipt with invalid chunk count 165 func (s *ReceiptValidationSuite) TestReceiptTooFewChunks() { 166 valSubgrph := s.ValidSubgraphFixture() 167 chunks := valSubgrph.Result.Chunks 168 valSubgrph.Result.Chunks = chunks[0 : len(chunks)-2] // drop the last chunk 169 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 170 unittest.WithResult(valSubgrph.Result)) 171 s.AddSubgraphFixtureToMempools(valSubgrph) 172 173 s.publicKey.On("Verify", 174 mock.Anything, 175 mock.Anything, 176 mock.Anything).Return(true, nil).Maybe() 177 178 err := s.receiptValidator.Validate(receipt) 179 s.Require().Error(err, "should reject with invalid chunks") 180 s.Assert().True(engine.IsInvalidInputError(err)) 181 } 182 183 // TestReceiptTooManyChunks tests that we reject receipt with more chunks than expected 184 func (s *ReceiptValidationSuite) TestReceiptTooManyChunks() { 185 valSubgrph := s.ValidSubgraphFixture() 186 chunks := valSubgrph.Result.Chunks 187 valSubgrph.Result.Chunks = append(chunks, chunks[len(chunks)-1]) // duplicate the last chunk 188 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 189 unittest.WithResult(valSubgrph.Result)) 190 s.AddSubgraphFixtureToMempools(valSubgrph) 191 192 s.publicKey.On("Verify", 193 mock.Anything, 194 mock.Anything, 195 mock.Anything).Return(true, nil).Maybe() 196 197 err := s.receiptValidator.Validate(receipt) 198 s.Require().Error(err, "should reject with invalid chunks") 199 s.Assert().True(engine.IsInvalidInputError(err)) 200 } 201 202 // TestReceiptChunkInvalidBlockID tests that we reject receipt with invalid chunk blockID 203 func (s *ReceiptValidationSuite) TestReceiptChunkInvalidBlockID() { 204 valSubgrph := s.ValidSubgraphFixture() 205 valSubgrph.Result.Chunks[0].BlockID = unittest.IdentifierFixture() 206 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 207 unittest.WithResult(valSubgrph.Result)) 208 s.AddSubgraphFixtureToMempools(valSubgrph) 209 210 s.publicKey.On("Verify", 211 mock.Anything, 212 mock.Anything, 213 mock.Anything).Return(true, nil).Maybe() 214 215 err := s.receiptValidator.Validate(receipt) 216 s.Require().Error(err, "should reject with invalid chunks") 217 s.Assert().True(engine.IsInvalidInputError(err)) 218 } 219 220 // TestReceiptInvalidCollectionIndex tests that we reject receipt with invalid chunk collection index 221 func (s *ReceiptValidationSuite) TestReceiptInvalidCollectionIndex() { 222 valSubgrph := s.ValidSubgraphFixture() 223 valSubgrph.Result.Chunks[0].CollectionIndex = 42 224 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 225 unittest.WithResult(valSubgrph.Result)) 226 s.AddSubgraphFixtureToMempools(valSubgrph) 227 228 s.publicKey.On("Verify", 229 mock.Anything, 230 mock.Anything, 231 mock.Anything).Return(true, nil).Maybe() 232 233 err := s.receiptValidator.Validate(receipt) 234 s.Require().Error(err, "should reject invalid collection index") 235 s.Assert().True(engine.IsInvalidInputError(err)) 236 } 237 238 // TestReceiptNoPreviousResult tests that we reject receipt with missing previous result 239 func (s *ReceiptValidationSuite) TestReceiptNoPreviousResult() { 240 valSubgrph := s.ValidSubgraphFixture() 241 // invalidate prev execution result, it will result in failing to lookup 242 // prev result during sub-graph check 243 valSubgrph.PreviousResult = unittest.ExecutionResultFixture() 244 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 245 unittest.WithResult(valSubgrph.Result)) 246 s.AddSubgraphFixtureToMempools(valSubgrph) 247 248 s.publicKey.On("Verify", 249 mock.Anything, 250 mock.Anything, 251 mock.Anything).Return(true, nil).Maybe() 252 253 err := s.receiptValidator.Validate(receipt) 254 s.Require().Error(err, "should reject invalid receipt") 255 s.Assert().True(engine.IsUnverifiableInputError(err), err) 256 } 257 258 // TestReceiptInvalidPreviousResult tests that we reject receipt with invalid previous result 259 func (s *ReceiptValidationSuite) TestReceiptInvalidPreviousResult() { 260 valSubgrph := s.ValidSubgraphFixture() 261 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 262 unittest.WithResult(valSubgrph.Result)) 263 s.AddSubgraphFixtureToMempools(valSubgrph) 264 265 // invalidate prev execution result blockID, this should fail because 266 // prev result points to wrong block 267 valSubgrph.PreviousResult.BlockID = unittest.IdentifierFixture() 268 269 s.publicKey.On("Verify", 270 mock.Anything, 271 mock.Anything, 272 mock.Anything).Return(true, nil).Maybe() 273 274 err := s.receiptValidator.Validate(receipt) 275 s.Require().Error(err, "should reject invalid previous result") 276 s.Assert().True(engine.IsInvalidInputError(err), err) 277 } 278 279 // TestReceiptInvalidResultChain tests that we reject receipts, 280 // where the start state does not match the parent result's end state 281 func (s *ReceiptValidationSuite) TestReceiptInvalidResultChain() { 282 valSubgrph := s.ValidSubgraphFixture() 283 receipt := unittest.ExecutionReceiptFixture(unittest.WithExecutorID(s.ExeID), 284 unittest.WithResult(valSubgrph.Result)) 285 s.AddSubgraphFixtureToMempools(valSubgrph) 286 287 // invalidate prev execution result blockID, this should fail because 288 // prev result points to wrong block 289 valSubgrph.PreviousResult.Chunks[len(valSubgrph.Result.Chunks)-1].EndState = unittest.StateCommitmentFixture() 290 291 s.publicKey.On("Verify", 292 mock.Anything, 293 mock.Anything, 294 mock.Anything).Return(true, nil).Maybe() 295 296 err := s.receiptValidator.Validate(receipt) 297 s.Require().Error(err, "should reject invalid previous result") 298 s.Assert().True(engine.IsInvalidInputError(err), err) 299 } 300 301 // TestMultiReceiptValidResultChain tests that multiple receipts and results 302 // within one block payload are accepted, where the receipts are building on 303 // top of each other (i.e. their results form a chain). 304 // Say B(A) means block B has receipt for A: 305 // - we have such chain in storage: G <- A <- B(A) <- C 306 // - if a child block of C payload contains receipts and results for (B,C) 307 // it should be accepted as valid 308 func (s *ReceiptValidationSuite) TestMultiReceiptValidResultChain() { 309 // assuming signatures are all good 310 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 311 312 // G <- A <- B <- C 313 blocks, result0, seal := unittest.ChainFixture(4) 314 s.SealsIndex[blocks[0].ID()] = seal 315 316 receipts := unittest.ReceiptChainFor(blocks, result0) 317 blockA, blockB, blockC := blocks[1], blocks[2], blocks[3] 318 receiptA, receiptB, receiptC := receipts[1], receipts[2], receipts[3] 319 320 blockA.Payload.Receipts = []*flow.ExecutionReceiptMeta{} 321 blockB.Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptA.Meta()} 322 blockB.Payload.Results = []*flow.ExecutionResult{&receiptA.ExecutionResult} 323 blockC.Payload.Receipts = []*flow.ExecutionReceiptMeta{} 324 // update block header so that blocks are chained together 325 unittest.ReconnectBlocksAndReceipts(blocks, receipts) 326 // assuming all receipts are executed by the correct executor 327 for _, r := range receipts { 328 r.ExecutorID = s.ExeID 329 } 330 331 for _, b := range blocks { 332 s.Extend(b) 333 } 334 s.PersistedResults[result0.ID()] = result0 335 336 candidate := unittest.BlockWithParentFixture(blockC.Header) 337 candidate.Payload = &flow.Payload{ 338 Receipts: []*flow.ExecutionReceiptMeta{receiptB.Meta(), receiptC.Meta()}, 339 Results: []*flow.ExecutionResult{&receiptB.ExecutionResult, &receiptC.ExecutionResult}, 340 } 341 342 err := s.receiptValidator.ValidatePayload(candidate) 343 s.Require().NoError(err) 344 } 345 346 // we have such chain in storage: G <- A <- B(A) <- C 347 // if a block payload contains (C,B_bad), they should be invalid 348 func (s *ReceiptValidationSuite) TestMultiReceiptInvalidParent() { 349 // assuming signatures are all good 350 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 351 352 // G <- A <- B <- C 353 blocks, result0, seal := unittest.ChainFixture(4) 354 s.SealsIndex[blocks[0].ID()] = seal 355 356 receipts := unittest.ReceiptChainFor(blocks, result0) 357 blockA, blockB, blockC := blocks[1], blocks[2], blocks[3] 358 receiptA := receipts[1] 359 receiptBInvalid := receipts[2] 360 receiptC := receipts[3] 361 blockA.Payload.Receipts = []*flow.ExecutionReceiptMeta{} 362 blockB.Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptA.Meta()} 363 blockB.Payload.Results = []*flow.ExecutionResult{&receiptA.ExecutionResult} 364 blockC.Payload.Receipts = []*flow.ExecutionReceiptMeta{} 365 // update block header so that blocks are chained together 366 unittest.ReconnectBlocksAndReceipts(blocks, receipts) 367 // assuming all receipts are executed by the correct executor 368 for _, r := range receipts { 369 r.ExecutorID = s.ExeID 370 } 371 372 for _, b := range blocks { 373 s.Extend(b) 374 } 375 s.PersistedResults[result0.ID()] = result0 376 377 // make receipt B as bad 378 receiptBInvalid.ExecutorID = unittest.IdentifierFixture() 379 380 candidate := unittest.BlockWithParentFixture(blockC.Header) 381 candidate.Payload = &flow.Payload{ 382 Receipts: []*flow.ExecutionReceiptMeta{receiptBInvalid.Meta(), receiptC.Meta()}, 383 Results: []*flow.ExecutionResult{&receiptBInvalid.ExecutionResult, &receiptC.ExecutionResult}, 384 } 385 386 // receiptB and receiptC 387 err := s.receiptValidator.ValidatePayload(candidate) 388 s.Require().Error(err) 389 require.True(s.T(), engine.IsInvalidInputError(err), err) 390 } 391 392 // Test that `ValidatePayload` will refuse payloads that contain receipts for blocks that 393 // are already sealed on the fork, but will accept receipts for blocks that are 394 // sealed on another fork. 395 func (s *ReceiptValidationSuite) TestValidationReceiptsForSealedBlock() { 396 // assuming signatures are all good 397 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 398 399 // create block2 400 block2 := unittest.BlockWithParentFixture(s.LatestSealedBlock.Header) 401 block2.SetPayload(flow.Payload{}) 402 s.Extend(block2) 403 404 block2Receipt := unittest.ExecutionReceiptFixture(unittest.WithResult( 405 unittest.ExecutionResultFixture(unittest.WithBlock(block2), 406 unittest.WithPreviousResult(*s.LatestExecutionResult)))) 407 408 // B1<--B2<--B3{R{B2)}<--B4{S(R(B2))}<--B5{R'(B2)} 409 410 // create block3 with a receipt for block2 411 block3 := unittest.BlockWithParentFixture(block2.Header) 412 block3.SetPayload(flow.Payload{ 413 Receipts: []*flow.ExecutionReceiptMeta{block2Receipt.Meta()}, 414 Results: []*flow.ExecutionResult{&block2Receipt.ExecutionResult}, 415 }) 416 s.Extend(block3) 417 418 // create a seal for block2 419 seal2 := unittest.Seal.Fixture(unittest.Seal.WithResult(&block2Receipt.ExecutionResult)) 420 421 // create block4 containing a seal for block2 422 block4 := unittest.BlockWithParentFixture(block3.Header) 423 block4.SetPayload(flow.Payload{ 424 Seals: []*flow.Seal{seal2}, 425 }) 426 s.Extend(block4) 427 428 // insert another receipt for block 2, which is now the highest sealed 429 // block, and ensure that the receipt is rejected 430 receipt := unittest.ExecutionReceiptFixture(unittest.WithResult( 431 unittest.ExecutionResultFixture(unittest.WithBlock(block2), 432 unittest.WithPreviousResult(*s.LatestExecutionResult))), 433 unittest.WithExecutorID(s.ExeID)) 434 block5 := unittest.BlockWithParentFixture(block4.Header) 435 block5.SetPayload(flow.Payload{ 436 Receipts: []*flow.ExecutionReceiptMeta{receipt.Meta()}, 437 Results: []*flow.ExecutionResult{&receipt.ExecutionResult}, 438 }) 439 440 err := s.receiptValidator.ValidatePayload(block5) 441 require.Error(s.T(), err) 442 require.True(s.T(), engine.IsInvalidInputError(err), err) 443 444 // B1<--B2<--B3{R{B2)}<--B4{S(R(B2))}<--B5{R'(B2)} 445 // | 446 // +---B6{R''(B2)} 447 448 // insert another receipt for B2 but in a separate fork. The fact that 449 // B2 is sealed on a separate fork should not cause the receipt to be 450 // rejected 451 block6 := unittest.BlockWithParentFixture(block2.Header) 452 block6.SetPayload(flow.Payload{ 453 Receipts: []*flow.ExecutionReceiptMeta{receipt.Meta()}, 454 Results: []*flow.ExecutionResult{&receipt.ExecutionResult}, 455 }) 456 err = s.receiptValidator.ValidatePayload(block6) 457 require.NoError(s.T(), err) 458 } 459 460 // Test that validator will accept payloads with receipts that are referring execution results 461 // which were incorporated in previous blocks of fork. 462 func (s *ReceiptValidationSuite) TestValidationReceiptForIncorporatedResult() { 463 // assuming signatures are all good 464 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 465 466 // create block2 467 block2 := unittest.BlockWithParentFixture(s.LatestSealedBlock.Header) 468 block2.SetPayload(flow.Payload{}) 469 s.Extend(block2) 470 471 executionResult := unittest.ExecutionResultFixture(unittest.WithBlock(block2), 472 unittest.WithPreviousResult(*s.LatestExecutionResult)) 473 firstReceipt := unittest.ExecutionReceiptFixture( 474 unittest.WithResult(executionResult), 475 unittest.WithExecutorID(s.ExeID)) 476 477 // B1<--B2<--B3{R{B2)}<--B4{(R'(B2))} 478 479 // create block3 with a receipt for block2 480 block3 := unittest.BlockWithParentFixture(block2.Header) 481 block3.SetPayload(flow.Payload{ 482 Receipts: []*flow.ExecutionReceiptMeta{firstReceipt.Meta()}, 483 Results: []*flow.ExecutionResult{&firstReceipt.ExecutionResult}, 484 }) 485 s.Extend(block3) 486 487 exe := unittest.IdentityFixture(unittest.WithRole(flow.RoleExecution)) 488 s.Identities[exe.NodeID] = exe 489 exe.StakingPubKey = s.publicKey // make sure the other exection node's signatures are valid 490 491 // insert another receipt for block 2, it's a receipt from another execution node 492 // for the same result 493 secondReceipt := unittest.ExecutionReceiptFixture( 494 unittest.WithResult(executionResult), 495 unittest.WithExecutorID(exe.NodeID)) 496 block5 := unittest.BlockWithParentFixture(block3.Header) 497 block5.SetPayload(flow.Payload{ 498 // no results, only receipt 499 Receipts: []*flow.ExecutionReceiptMeta{secondReceipt.Meta()}, 500 }) 501 502 err := s.receiptValidator.ValidatePayload(block5) 503 require.NoError(s.T(), err) 504 } 505 506 // TestValidationReceiptWithoutIncorporatedResult verifies that receipts must commit 507 // to results that are included in the respective fork. Specifically, we test that 508 // the counter-example is rejected: 509 // - we have the chain in storage: 510 // . G <- A <- B 511 // . ^- C(Result[A], ReceiptMeta[A]) 512 // here, block C contains the result _and_ the receipt Meta-data for block A 513 // - now receive the new block X: G <- A <- B <- X(ReceiptMeta[A]) 514 // Note that X only contains the receipt for A, but _not_ the result. 515 // 516 // Block X must be considered invalid, because confirming validity of 517 // ReceiptMeta[A] requires information _not_ included in the fork. 518 func (s *ReceiptValidationSuite) TestValidationReceiptWithoutIncorporatedResult() { 519 // assuming signatures are all good 520 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 521 522 // create block A 523 blockA := unittest.BlockWithParentFixture(s.LatestSealedBlock.Header) // for block G, we use the LatestSealedBlock 524 s.Extend(blockA) 525 526 // result for A; and receipt for A 527 resultA := unittest.ExecutionResultFixture(unittest.WithBlock(blockA), unittest.WithPreviousResult(*s.LatestExecutionResult)) 528 receiptA := unittest.ExecutionReceiptFixture(unittest.WithResult(resultA), unittest.WithExecutorID(s.ExeID)) 529 530 // create block B and block C 531 blockB := unittest.BlockWithParentFixture(blockA.Header) 532 blockC := unittest.BlockWithParentFixture(blockA.Header) 533 blockC.SetPayload(flow.Payload{ 534 Receipts: []*flow.ExecutionReceiptMeta{receiptA.Meta()}, 535 Results: []*flow.ExecutionResult{resultA}, 536 }) 537 s.Extend(blockB) 538 s.Extend(blockC) 539 540 // create block X: 541 blockX := unittest.BlockWithParentFixture(blockB.Header) 542 blockX.SetPayload(flow.Payload{ 543 Receipts: []*flow.ExecutionReceiptMeta{receiptA.Meta()}, 544 }) 545 546 err := s.receiptValidator.ValidatePayload(blockX) 547 require.Error(s.T(), err) 548 require.True(s.T(), engine.IsInvalidInputError(err), err) 549 } 550 551 // TestPayloadWithExecutionFork checks that the Receipt Validator only 552 // accepts results that decent from the sealed result. Specifically, we test that 553 // the counter-example is rejected: 554 // 555 // - we have the chain in storage: 556 // 557 // . S <- A(Result[S]_1, Result[S]_2, ReceiptMeta[S]_1, ReceiptMeta[S]_2) 558 // . <- B(Seal for Result[S]_2) 559 // . <- X(Result[A]_1, Result[A]_2, Result[A]_3, 560 // . ReceiptMeta[A]_1, ReceiptMeta[A]_2, ReceiptMeta[A]_3) 561 // 562 // - Note that we are explicitly testing the handling of an execution fork _before_ 563 // and _after_ the sealed result 564 // 565 // . Blocks: S <----------- A 566 // . Results: Result[S]_1 <- Result[A]_1 :: the root of this execution tree conflicts with sealed result 567 // . Result[S]_2 <- Result[A]_2 :: the root of this execution tree is sealed 568 // . ^- Result[A]_3 569 // 570 // Expected Behaviour: 571 // In the fork which X extends, Result[S]_2 has been sealed. Hence, it should be: 572 // (i) illegal to include Result[A]_1, because it is _not_ derived from the sealed result. 573 // (ii) legal to include only results Result[A]_2 and Result[A]_3, as they are derived from the sealed result. 574 func (s *ReceiptValidationSuite) TestPayloadWithExecutionFork() { 575 // assuming signatures are all good 576 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 577 578 // block S: we use s.LatestSealedBlock; its result is s.LatestExecutionResult 579 blockS := s.LatestSealedBlock 580 resultS1 := s.LatestExecutionResult 581 receiptS1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resultS1), unittest.WithExecutorID(s.ExeID)) 582 resultS2 := unittest.ExecutionResultFixture(unittest.WithBlock(&blockS)) 583 receiptS2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resultS2), unittest.WithExecutorID(s.ExeID)) 584 585 // create block A, including results and receipts for it 586 blockA := unittest.BlockWithParentFixture(blockS.Header) 587 blockA.SetPayload(flow.Payload{ 588 Results: []*flow.ExecutionResult{resultS1, resultS2}, 589 Receipts: []*flow.ExecutionReceiptMeta{receiptS1.Meta(), receiptS2.Meta()}, 590 }) 591 s.Extend(blockA) 592 593 // create block B 594 blockB := unittest.BlockWithParentFixture(blockA.Header) 595 sealResultS2 := unittest.Seal.Fixture(unittest.Seal.WithBlock(blockS.Header), unittest.Seal.WithResult(resultS2)) 596 blockB.SetPayload(flow.Payload{ 597 Seals: []*flow.Seal{sealResultS2}, 598 }) 599 s.Extend(blockB) 600 601 // create Result[A]_1, Result[A]_2, Result[A]_3 and their receipts 602 resultA1 := unittest.ExecutionResultFixture(unittest.WithBlock(blockA), unittest.WithPreviousResult(*resultS1)) 603 receiptA1 := unittest.ExecutionReceiptFixture(unittest.WithResult(resultA1), unittest.WithExecutorID(s.ExeID)) 604 resultA2 := unittest.ExecutionResultFixture(unittest.WithBlock(blockA), unittest.WithPreviousResult(*resultS2)) 605 receiptA2 := unittest.ExecutionReceiptFixture(unittest.WithResult(resultA2), unittest.WithExecutorID(s.ExeID)) 606 resultA3 := unittest.ExecutionResultFixture(unittest.WithBlock(blockA), unittest.WithPreviousResult(*resultS2)) 607 receiptA3 := unittest.ExecutionReceiptFixture(unittest.WithResult(resultA3), unittest.WithExecutorID(s.ExeID)) 608 609 // SCENARIO (i): a block containing Result[A]_1 should fail validation 610 blockX := unittest.BlockWithParentFixture(blockB.Header) 611 blockX.SetPayload(flow.Payload{ 612 Results: []*flow.ExecutionResult{resultA1, resultA2, resultA3}, 613 Receipts: []*flow.ExecutionReceiptMeta{receiptA1.Meta(), receiptA2.Meta(), receiptA3.Meta()}, 614 }) 615 err := s.receiptValidator.ValidatePayload(blockX) 616 require.Error(s.T(), err) 617 require.True(s.T(), engine.IsInvalidInputError(err), err) 618 619 // SCENARIO (ii): a block containing only results Result[A]_2 and Result[A]_3 should pass validation 620 blockX = unittest.BlockWithParentFixture(blockB.Header) 621 blockX.SetPayload(flow.Payload{ 622 Results: []*flow.ExecutionResult{resultA2, resultA3}, 623 Receipts: []*flow.ExecutionReceiptMeta{receiptA2.Meta(), receiptA3.Meta()}, 624 }) 625 err = s.receiptValidator.ValidatePayload(blockX) 626 require.NoError(s.T(), err) 627 } 628 629 // TestMultiLevelExecutionTree verifies that a result is accepted that 630 // extends a multi-level execution tree : 631 // - Let S be the latest sealed block 632 // - we have the chain in storage: 633 // S <- A <- B(Result[A], ReceiptMeta[A]) <- C(Result[B], ReceiptMeta[B]) 634 // - now receive the new block X: 635 // S <- A <- B(Result[A], ReceiptMeta[A]) <- C(Result[B], ReceiptMeta[B]) <- X(Result[C], ReceiptMeta[C]) 636 // 637 // Block X should be considered valid, as it extends the 638 // Execution Tree with root latest sealed Result (i.e. result sealed for S) 639 func (s *ReceiptValidationSuite) TestMultiLevelExecutionTree() { 640 // assuming signatures are all good 641 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 642 643 // create block A, including result and receipt for it 644 blockA := unittest.BlockWithParentFixture(s.LatestSealedBlock.Header) 645 resultA := unittest.ExecutionResultFixture(unittest.WithBlock(blockA), unittest.WithPreviousResult(*s.LatestExecutionResult)) 646 receiptA := unittest.ExecutionReceiptFixture(unittest.WithResult(resultA), unittest.WithExecutorID(s.ExeID)) 647 s.Extend(blockA) 648 649 // create block B, including result and receipt for it 650 blockB := unittest.BlockWithParentFixture(blockA.Header) 651 blockB.SetPayload(flow.Payload{ 652 Receipts: []*flow.ExecutionReceiptMeta{receiptA.Meta()}, 653 Results: []*flow.ExecutionResult{resultA}, 654 }) 655 resultB := unittest.ExecutionResultFixture(unittest.WithBlock(blockB), unittest.WithPreviousResult(*resultA)) 656 receiptB := unittest.ExecutionReceiptFixture(unittest.WithResult(resultB), unittest.WithExecutorID(s.ExeID)) 657 s.Extend(blockB) 658 659 // create block C, including result and receipt for it 660 blockC := unittest.BlockWithParentFixture(blockB.Header) 661 blockC.SetPayload(flow.Payload{ 662 Receipts: []*flow.ExecutionReceiptMeta{receiptB.Meta()}, 663 Results: []*flow.ExecutionResult{resultB}, 664 }) 665 resultC := unittest.ExecutionResultFixture(unittest.WithBlock(blockC), unittest.WithPreviousResult(*resultB)) 666 receiptC := unittest.ExecutionReceiptFixture(unittest.WithResult(resultC), unittest.WithExecutorID(s.ExeID)) 667 s.Extend(blockC) 668 669 // create block X: 670 blockX := unittest.BlockWithParentFixture(blockC.Header) 671 blockX.SetPayload(flow.Payload{ 672 Receipts: []*flow.ExecutionReceiptMeta{receiptC.Meta()}, 673 Results: []*flow.ExecutionResult{resultC}, 674 }) 675 676 err := s.receiptValidator.ValidatePayload(blockX) 677 require.NoError(s.T(), err) 678 } 679 680 // Test that validator will reject payloads that contain receipts for blocks that 681 // are not on the fork 682 // 683 // B1<--B2<--B3 684 // | 685 // +----B4{R(B3)} 686 func (s *ReceiptValidationSuite) TestValidationReceiptsBlockNotOnFork() { 687 // create block2 688 block2 := unittest.BlockWithParentFixture(s.LatestFinalizedBlock.Header) 689 block2.Payload.Guarantees = nil 690 block2.Header.PayloadHash = block2.Payload.Hash() 691 s.Extend(block2) 692 693 // create block3 694 block3 := unittest.BlockWithParentFixture(block2.Header) 695 block3.SetPayload(flow.Payload{}) 696 s.Extend(block3) 697 698 block3Receipt := unittest.ReceiptForBlockFixture(block3) 699 700 block4 := unittest.BlockWithParentFixture(block2.Header) 701 block4.SetPayload(flow.Payload{ 702 Receipts: []*flow.ExecutionReceiptMeta{block3Receipt.Meta()}, 703 Results: []*flow.ExecutionResult{&block3Receipt.ExecutionResult}, 704 }) 705 err := s.receiptValidator.ValidatePayload(block4) 706 require.Error(s.T(), err) 707 require.True(s.T(), engine.IsInvalidInputError(err), err) 708 } 709 710 // Test that Extend will refuse payloads that contain duplicate receipts, where 711 // duplicates can be in another block on the fork, or within the payload. 712 func (s *ReceiptValidationSuite) TestExtendReceiptsDuplicate() { 713 714 block2 := unittest.BlockWithParentFixture(s.LatestFinalizedBlock.Header) 715 block2.SetPayload(flow.Payload{}) 716 s.Extend(block2) 717 718 receipt := unittest.ReceiptForBlockFixture(block2) 719 720 // B1 <- B2 <- B3{R(B2)} <- B4{R(B2)} 721 s.T().Run("duplicate receipt in different block", func(t *testing.T) { 722 block3 := unittest.BlockWithParentFixture(block2.Header) 723 block3.SetPayload(flow.Payload{ 724 Receipts: []*flow.ExecutionReceiptMeta{receipt.Meta()}, 725 Results: []*flow.ExecutionResult{&receipt.ExecutionResult}, 726 }) 727 s.Extend(block3) 728 729 block4 := unittest.BlockWithParentFixture(block3.Header) 730 block4.SetPayload(flow.Payload{ 731 Receipts: []*flow.ExecutionReceiptMeta{receipt.Meta()}, 732 Results: []*flow.ExecutionResult{&receipt.ExecutionResult}, 733 }) 734 err := s.receiptValidator.ValidatePayload(block4) 735 require.Error(t, err) 736 require.True(t, engine.IsInvalidInputError(err), err) 737 }) 738 739 // B1 <- B2 <- B3{R(B2), R(B2)} 740 s.T().Run("duplicate receipt in same block", func(t *testing.T) { 741 block3 := unittest.BlockWithParentFixture(block2.Header) 742 block3.SetPayload(flow.Payload{ 743 Receipts: []*flow.ExecutionReceiptMeta{ 744 receipt.Meta(), 745 receipt.Meta(), 746 }, 747 Results: []*flow.ExecutionResult{ 748 &receipt.ExecutionResult, 749 }, 750 }) 751 err := s.receiptValidator.ValidatePayload(block3) 752 require.Error(t, err) 753 require.True(t, engine.IsInvalidInputError(err), err) 754 }) 755 } 756 757 // `TestValidateReceiptAfterBootstrap` tests a special case when we try to produce a new block 758 // after genesis with empty payload. 759 func (s *ReceiptValidationSuite) TestValidateReceiptAfterBootstrap() { 760 // assuming signatures are all good 761 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 762 763 // G 764 blocks, result0, seal := unittest.ChainFixture(0) 765 s.SealsIndex[blocks[0].ID()] = seal 766 767 for _, b := range blocks { 768 s.Extend(b) 769 } 770 s.PersistedResults[result0.ID()] = result0 771 772 candidate := unittest.BlockWithParentFixture(blocks[0].Header) 773 err := s.receiptValidator.ValidatePayload(candidate) 774 s.Require().NoError(err) 775 } 776 777 // TestValidateReceiptResultWithoutReceipt tests a case when a malicious leader incorporates a made-up execution result 778 // into their proposal. ReceiptValidator must ensure that for each result included in the block, there must be 779 // at least one receipt included in that block as well. 780 func (s *ReceiptValidationSuite) TestValidateReceiptResultWithoutReceipt() { 781 // assuming signatures are all good 782 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 783 784 // G <- A <- B 785 blocks, result0, seal := unittest.ChainFixture(2) 786 s.SealsIndex[blocks[0].ID()] = seal 787 788 receipts := unittest.ReceiptChainFor(blocks, result0) 789 blockA, blockB := blocks[1], blocks[2] 790 receiptA, receiptB := receipts[1], receipts[2] 791 792 blockA.Payload.Receipts = []*flow.ExecutionReceiptMeta{} 793 blockB.Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptA.Meta()} 794 blockB.Payload.Results = []*flow.ExecutionResult{&receiptA.ExecutionResult} 795 // update block header so that blocks are chained together 796 unittest.ReconnectBlocksAndReceipts(blocks, receipts) 797 // assuming all receipts are executed by the correct executor 798 for _, r := range receipts { 799 r.ExecutorID = s.ExeID 800 } 801 802 for _, b := range blocks { 803 s.Extend(b) 804 } 805 s.PersistedResults[result0.ID()] = result0 806 807 candidate := unittest.BlockWithParentFixture(blockB.Header) 808 candidate.Payload = &flow.Payload{ 809 Receipts: []*flow.ExecutionReceiptMeta{}, 810 Results: []*flow.ExecutionResult{&receiptB.ExecutionResult}, 811 } 812 813 err := s.receiptValidator.ValidatePayload(candidate) 814 s.Require().Error(err) 815 s.Require().True(engine.IsInvalidInputError(err)) 816 } 817 818 // TestValidateReceiptResultHasEnoughReceipts tests the happy path of a block proposal, where a leader 819 // includes multiple Execution Receipts that commit to the same result. In this case, the Flow protocol 820 // prescribes that 821 // - the Execution Result is only incorporated once 822 // - from each Receipt the `ExecutionReceiptMeta` is to be included. 823 // 824 // The validator is expected to accept such payload as valid. 825 func (s *ReceiptValidationSuite) TestValidateReceiptResultHasEnoughReceipts() { 826 k := uint(5) 827 // assuming signatures are all good 828 s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 829 830 // G <- A <- B 831 blocks, result0, seal := unittest.ChainFixture(2) 832 s.SealsIndex[blocks[0].ID()] = seal 833 834 receipts := unittest.ReceiptChainFor(blocks, result0) 835 blockA, blockB := blocks[1], blocks[2] 836 receiptA, receiptB := receipts[1], receipts[2] 837 838 blockA.Payload.Receipts = []*flow.ExecutionReceiptMeta{} 839 blockB.Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptA.Meta()} 840 blockB.Payload.Results = []*flow.ExecutionResult{&receiptA.ExecutionResult} 841 // update block header so that blocks are chained together 842 unittest.ReconnectBlocksAndReceipts(blocks, receipts) 843 // assuming all receipts are executed by the correct executor 844 for _, r := range receipts { 845 r.ExecutorID = s.ExeID 846 } 847 848 for _, b := range blocks { 849 s.Extend(b) 850 } 851 s.PersistedResults[result0.ID()] = result0 852 853 candidateReceipts := []*flow.ExecutionReceiptMeta{receiptB.Meta()} 854 // add k-1 more receipts for the same execution result 855 for i := uint(1); i < k; i++ { 856 // use base receipt and change the executor ID, we don't care about signatures since we are not validating them 857 receipt := *receiptB.Meta() 858 // create a mock executor which submitted the receipt 859 executor := unittest.IdentityFixture(unittest.WithRole(flow.RoleExecution), unittest.WithStakingPubKey(s.publicKey)) 860 receipt.ExecutorID = executor.NodeID 861 // update local identity table so the receipt is considered valid 862 s.Identities[executor.NodeID] = executor 863 candidateReceipts = append(candidateReceipts, &receipt) 864 } 865 866 candidate := unittest.BlockWithParentFixture(blockB.Header) 867 candidate.Payload = &flow.Payload{ 868 Receipts: candidateReceipts, 869 Results: []*flow.ExecutionResult{&receiptB.ExecutionResult}, 870 } 871 872 err := s.receiptValidator.ValidatePayload(candidate) 873 s.Require().NoError(err) 874 }