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