github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/fvm/environment/programs_test.go (about) 1 package environment_test 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/onflow/cadence/runtime/common" 10 "github.com/stretchr/testify/require" 11 12 "github.com/onflow/flow-go/fvm" 13 "github.com/onflow/flow-go/fvm/environment" 14 "github.com/onflow/flow-go/fvm/storage" 15 "github.com/onflow/flow-go/fvm/storage/derived" 16 "github.com/onflow/flow-go/fvm/storage/snapshot" 17 "github.com/onflow/flow-go/fvm/storage/state" 18 "github.com/onflow/flow-go/model/flow" 19 ) 20 21 var ( 22 addressA = flow.HexToAddress("0a") 23 addressB = flow.HexToAddress("0b") 24 addressC = flow.HexToAddress("0c") 25 26 contractALocation = common.AddressLocation{ 27 Address: common.MustBytesToAddress(addressA.Bytes()), 28 Name: "A", 29 } 30 contractA2Location = common.AddressLocation{ 31 Address: common.MustBytesToAddress(addressA.Bytes()), 32 Name: "A2", 33 } 34 35 contractBLocation = common.AddressLocation{ 36 Address: common.MustBytesToAddress(addressB.Bytes()), 37 Name: "B", 38 } 39 40 contractCLocation = common.AddressLocation{ 41 Address: common.MustBytesToAddress(addressC.Bytes()), 42 Name: "C", 43 } 44 45 contractA0Code = ` 46 access(all) contract A { 47 access(all) struct interface Foo{} 48 49 access(all) fun hello(): String { 50 return "bad version" 51 } 52 } 53 ` 54 55 contractACode = ` 56 access(all) contract A { 57 access(all) struct interface Foo{} 58 59 access(all) fun hello(): String { 60 return "hello from A" 61 } 62 } 63 ` 64 65 contractA2Code = ` 66 access(all) contract A2 { 67 access(all) struct interface Foo{} 68 69 access(all) fun hello(): String { 70 return "hello from A2" 71 } 72 } 73 ` 74 75 contractABreakingCode = ` 76 access(all) contract A { 77 access(all) struct interface Foo{ 78 access(all) fun hello() 79 } 80 81 access(all) fun hello(): String { 82 return "hello from A with breaking change" 83 } 84 } 85 ` 86 87 contractBCode = ` 88 import 0xa 89 90 access(all) contract B { 91 access(all) struct Bar : A.Foo {} 92 93 access(all) fun hello(): String { 94 return "hello from B but also ".concat(A.hello()) 95 } 96 } 97 ` 98 99 contractCCode = ` 100 import B from 0xb 101 import A from 0xa 102 103 access(all) contract C { 104 access(all) struct Bar : A.Foo {} 105 106 access(all) fun hello(): String { 107 return "hello from C, ".concat(B.hello()) 108 } 109 } 110 ` 111 ) 112 113 func setupProgramsTest(t *testing.T) snapshot.SnapshotTree { 114 blockDatabase := storage.NewBlockDatabase(nil, 0, nil) 115 txnState, err := blockDatabase.NewTransaction(0, state.DefaultParameters()) 116 require.NoError(t, err) 117 118 accounts := environment.NewAccounts(txnState) 119 120 err = accounts.Create(nil, addressA) 121 require.NoError(t, err) 122 123 err = accounts.Create(nil, addressB) 124 require.NoError(t, err) 125 126 err = accounts.Create(nil, addressC) 127 require.NoError(t, err) 128 129 executionSnapshot, err := txnState.FinalizeMainTransaction() 130 require.NoError(t, err) 131 132 return snapshot.NewSnapshotTree(nil).Append(executionSnapshot) 133 } 134 135 func getTestContract( 136 snapshot snapshot.StorageSnapshot, 137 location common.AddressLocation, 138 ) ( 139 []byte, 140 error, 141 ) { 142 env := environment.NewScriptEnvironmentFromStorageSnapshot( 143 environment.DefaultEnvironmentParams(), 144 snapshot) 145 return env.GetAccountContractCode(location) 146 } 147 148 func Test_Programs(t *testing.T) { 149 vm := fvm.NewVirtualMachine() 150 derivedBlockData := derived.NewEmptyDerivedBlockData(0) 151 152 mainSnapshot := setupProgramsTest(t) 153 154 context := fvm.NewContext( 155 fvm.WithContractDeploymentRestricted(false), 156 fvm.WithAuthorizationChecksEnabled(false), 157 fvm.WithSequenceNumberCheckAndIncrementEnabled(false), 158 fvm.WithCadenceLogging(true), 159 fvm.WithDerivedBlockData(derivedBlockData)) 160 161 var contractASnapshot *snapshot.ExecutionSnapshot 162 var contractBSnapshot *snapshot.ExecutionSnapshot 163 var txASnapshot *snapshot.ExecutionSnapshot 164 165 t.Run("contracts can be updated", func(t *testing.T) { 166 retrievedContractA, err := getTestContract( 167 mainSnapshot, 168 contractALocation) 169 require.NoError(t, err) 170 require.Empty(t, retrievedContractA) 171 172 // deploy contract A0 173 executionSnapshot, output, err := vm.Run( 174 context, 175 fvm.Transaction( 176 contractDeployTx("A", contractA0Code, addressA), 177 derivedBlockData.NextTxIndexForTestingOnly()), 178 mainSnapshot) 179 require.NoError(t, err) 180 require.NoError(t, output.Err) 181 182 mainSnapshot = mainSnapshot.Append(executionSnapshot) 183 184 retrievedContractA, err = getTestContract( 185 mainSnapshot, 186 contractALocation) 187 require.NoError(t, err) 188 189 require.Equal(t, contractA0Code, string(retrievedContractA)) 190 191 // deploy contract A 192 executionSnapshot, output, err = vm.Run( 193 context, 194 fvm.Transaction( 195 updateContractTx("A", contractACode, addressA), 196 derivedBlockData.NextTxIndexForTestingOnly()), 197 mainSnapshot) 198 require.NoError(t, err) 199 require.NoError(t, output.Err) 200 201 mainSnapshot = mainSnapshot.Append(executionSnapshot) 202 203 retrievedContractA, err = getTestContract( 204 mainSnapshot, 205 contractALocation) 206 require.NoError(t, err) 207 208 require.Equal(t, contractACode, string(retrievedContractA)) 209 210 }) 211 t.Run("register touches are captured for simple contract A", func(t *testing.T) { 212 t.Log("---------- Real transaction here ------------") 213 214 // run a TX using contract A 215 216 loadedCode := false 217 execASnapshot := snapshot.NewReadFuncStorageSnapshot( 218 func(id flow.RegisterID) (flow.RegisterValue, error) { 219 expectedId := flow.ContractRegisterID( 220 flow.BytesToAddress([]byte(id.Owner)), 221 "A") 222 if id == expectedId { 223 loadedCode = true 224 } 225 226 return mainSnapshot.Get(id) 227 }) 228 229 executionSnapshotA, output, err := vm.Run( 230 context, 231 fvm.Transaction( 232 callTx("A", addressA), 233 derivedBlockData.NextTxIndexForTestingOnly()), 234 execASnapshot) 235 require.NoError(t, err) 236 require.NoError(t, output.Err) 237 238 mainSnapshot = mainSnapshot.Append(executionSnapshotA) 239 240 // make sure tx was really run 241 require.Contains(t, output.Logs, "\"hello from A\"") 242 243 // Make sure the code has been loaded from storage 244 require.True(t, loadedCode) 245 246 entry := derivedBlockData.GetProgramForTestingOnly(contractALocation) 247 require.NotNil(t, entry) 248 cached := derivedBlockData.CachedPrograms() 249 require.Equal(t, 1, cached) 250 251 // assert dependencies are correct 252 require.Equal(t, 1, entry.Value.Dependencies.Count()) 253 require.True(t, entry.Value.Dependencies.ContainsLocation(contractALocation)) 254 255 // assert some reads were recorded (at least loading of code) 256 require.NotEmpty(t, entry.ExecutionSnapshot.ReadSet) 257 258 contractASnapshot = entry.ExecutionSnapshot 259 txASnapshot = executionSnapshotA 260 261 // execute transaction again, this time make sure it doesn't load code 262 execA2Snapshot := snapshot.NewReadFuncStorageSnapshot( 263 func(id flow.RegisterID) (flow.RegisterValue, error) { 264 notId := flow.ContractRegisterID( 265 flow.BytesToAddress([]byte(id.Owner)), 266 "A") 267 // this time we fail if a read of code occurs 268 require.NotEqual(t, id, notId) 269 270 return mainSnapshot.Get(id) 271 }) 272 273 executionSnapshotA2, output, err := vm.Run( 274 context, 275 fvm.Transaction( 276 callTx("A", addressA), 277 derivedBlockData.NextTxIndexForTestingOnly()), 278 execA2Snapshot) 279 require.NoError(t, err) 280 require.NoError(t, output.Err) 281 282 mainSnapshot = mainSnapshot.Append(executionSnapshotA2) 283 284 require.Contains(t, output.Logs, "\"hello from A\"") 285 286 // same transaction should produce the exact same execution snapshots 287 // but only because we don't do any conditional update in a tx 288 compareExecutionSnapshots(t, executionSnapshotA, executionSnapshotA2) 289 }) 290 291 t.Run("deploying another contract invalidates dependant programs", func(t *testing.T) { 292 // deploy contract B 293 executionSnapshot, output, err := vm.Run( 294 context, 295 fvm.Transaction( 296 contractDeployTx("B", contractBCode, addressB), 297 derivedBlockData.NextTxIndexForTestingOnly()), 298 mainSnapshot) 299 require.NoError(t, err) 300 require.NoError(t, output.Err) 301 302 mainSnapshot = mainSnapshot.Append(executionSnapshot) 303 304 // b and c are invalid 305 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 306 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 307 // a is still valid 308 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 309 310 require.Nil(t, entryB) 311 require.Nil(t, entryC) 312 require.NotNil(t, entryA) 313 314 cached := derivedBlockData.CachedPrograms() 315 require.Equal(t, 1, cached) 316 }) 317 318 t.Run("contract B imports contract A", func(t *testing.T) { 319 320 // programs should have no entries for A and B, as per previous test 321 322 // run a TX using contract B 323 324 executionSnapshotB, output, err := vm.Run( 325 context, 326 fvm.Transaction( 327 callTx("B", addressB), 328 derivedBlockData.NextTxIndexForTestingOnly()), 329 mainSnapshot) 330 require.NoError(t, err) 331 332 mainSnapshot = mainSnapshot.Append(executionSnapshotB) 333 334 require.Contains(t, output.Logs, "\"hello from B but also hello from A\"") 335 336 entry := derivedBlockData.GetProgramForTestingOnly(contractALocation) 337 require.NotNil(t, entry) 338 339 // state should be essentially the same as one which we got in tx with contract A 340 require.Equal(t, contractASnapshot, entry.ExecutionSnapshot) 341 342 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 343 require.NotNil(t, entryB) 344 345 // assert dependencies are correct 346 require.Equal(t, 2, entryB.Value.Dependencies.Count()) 347 require.NotNil(t, entryB.Value.Dependencies.ContainsLocation(contractALocation)) 348 require.NotNil(t, entryB.Value.Dependencies.ContainsLocation(contractBLocation)) 349 350 // program B should contain all the registers used by program A, as it depends on it 351 contractBSnapshot = entryB.ExecutionSnapshot 352 353 require.Empty(t, contractASnapshot.WriteSet) 354 355 for id := range contractASnapshot.ReadSet { 356 _, ok := contractBSnapshot.ReadSet[id] 357 require.True(t, ok) 358 } 359 360 // rerun transaction 361 362 // execute transaction again, this time make sure it doesn't load code 363 execB2Snapshot := snapshot.NewReadFuncStorageSnapshot( 364 func(id flow.RegisterID) (flow.RegisterValue, error) { 365 idA := flow.ContractRegisterID( 366 flow.BytesToAddress([]byte(id.Owner)), 367 "A") 368 idB := flow.ContractRegisterID( 369 flow.BytesToAddress([]byte(id.Owner)), 370 "B") 371 // this time we fail if a read of code occurs 372 require.NotEqual(t, id.Key, idA.Key) 373 require.NotEqual(t, id.Key, idB.Key) 374 375 return mainSnapshot.Get(id) 376 }) 377 378 executionSnapshotB2, output, err := vm.Run( 379 context, 380 fvm.Transaction( 381 callTx("B", addressB), 382 derivedBlockData.NextTxIndexForTestingOnly()), 383 execB2Snapshot) 384 require.NoError(t, err) 385 require.NoError(t, output.Err) 386 387 require.Contains(t, output.Logs, "\"hello from B but also hello from A\"") 388 389 mainSnapshot = mainSnapshot.Append(executionSnapshotB2) 390 391 compareExecutionSnapshots(t, executionSnapshotB, executionSnapshotB2) 392 }) 393 394 t.Run("deploying new contract A2 invalidates B because of * imports", func(t *testing.T) { 395 // deploy contract A2 396 executionSnapshot, output, err := vm.Run( 397 context, 398 fvm.Transaction( 399 contractDeployTx("A2", contractA2Code, addressA), 400 derivedBlockData.NextTxIndexForTestingOnly()), 401 mainSnapshot) 402 require.NoError(t, err) 403 require.NoError(t, output.Err) 404 405 mainSnapshot = mainSnapshot.Append(executionSnapshot) 406 407 // a, b and c are invalid 408 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 409 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 410 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 411 412 require.Nil(t, entryB) // B could have star imports to 0xa, so it's invalidated 413 require.Nil(t, entryC) // still invalid 414 require.Nil(t, entryA) // A could have star imports to 0xa, so it's invalidated 415 416 cached := derivedBlockData.CachedPrograms() 417 require.Equal(t, 0, cached) 418 }) 419 420 t.Run("contract B imports contract A and A2 because of * import", func(t *testing.T) { 421 422 // programs should have no entries for A and B, as per previous test 423 424 // run a TX using contract B 425 426 executionSnapshotB, output, err := vm.Run( 427 context, 428 fvm.Transaction( 429 callTx("B", addressB), 430 derivedBlockData.NextTxIndexForTestingOnly()), 431 mainSnapshot) 432 require.NoError(t, err) 433 require.NoError(t, output.Err) 434 435 require.Contains(t, output.Logs, "\"hello from B but also hello from A\"") 436 437 mainSnapshot = mainSnapshot.Append(executionSnapshotB) 438 439 entry := derivedBlockData.GetProgramForTestingOnly(contractALocation) 440 require.NotNil(t, entry) 441 442 // state should be essentially the same as one which we got in tx with contract A 443 require.Equal(t, contractASnapshot, entry.ExecutionSnapshot) 444 445 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 446 require.NotNil(t, entryB) 447 448 // assert dependencies are correct 449 require.Equal(t, 3, entryB.Value.Dependencies.Count()) 450 require.NotNil(t, entryB.Value.Dependencies.ContainsLocation(contractALocation)) 451 require.NotNil(t, entryB.Value.Dependencies.ContainsLocation(contractBLocation)) 452 require.NotNil(t, entryB.Value.Dependencies.ContainsLocation(contractA2Location)) 453 454 // program B should contain all the registers used by program A, as it depends on it 455 contractBSnapshot = entryB.ExecutionSnapshot 456 457 require.Empty(t, contractASnapshot.WriteSet) 458 459 for id := range contractASnapshot.ReadSet { 460 _, ok := contractBSnapshot.ReadSet[id] 461 require.True(t, ok) 462 } 463 464 // rerun transaction 465 466 // execute transaction again, this time make sure it doesn't load code 467 execB2Snapshot := snapshot.NewReadFuncStorageSnapshot( 468 func(id flow.RegisterID) (flow.RegisterValue, error) { 469 idA := flow.ContractRegisterID( 470 flow.BytesToAddress([]byte(id.Owner)), 471 "A") 472 idA2 := flow.ContractRegisterID( 473 flow.BytesToAddress([]byte(id.Owner)), 474 "A2") 475 idB := flow.ContractRegisterID( 476 flow.BytesToAddress([]byte(id.Owner)), 477 "B") 478 // this time we fail if a read of code occurs 479 require.NotEqual(t, id.Key, idA.Key) 480 require.NotEqual(t, id.Key, idA2.Key) 481 require.NotEqual(t, id.Key, idB.Key) 482 483 return mainSnapshot.Get(id) 484 }) 485 486 executionSnapshotB2, output, err := vm.Run( 487 context, 488 fvm.Transaction( 489 callTx("B", addressB), 490 derivedBlockData.NextTxIndexForTestingOnly()), 491 execB2Snapshot) 492 require.NoError(t, err) 493 require.NoError(t, output.Err) 494 495 require.Contains(t, output.Logs, "\"hello from B but also hello from A\"") 496 497 mainSnapshot = mainSnapshot.Append(executionSnapshotB2) 498 499 compareExecutionSnapshots(t, executionSnapshotB, executionSnapshotB2) 500 }) 501 502 t.Run("contract A runs from cache after program B has been loaded", func(t *testing.T) { 503 504 // at this point programs cache should contain data for contract A 505 // only because contract B has been called 506 507 execASnapshot := snapshot.NewReadFuncStorageSnapshot( 508 func(id flow.RegisterID) (flow.RegisterValue, error) { 509 notId := flow.ContractRegisterID( 510 flow.BytesToAddress([]byte(id.Owner)), 511 "A") 512 require.NotEqual(t, id, notId) 513 return mainSnapshot.Get(id) 514 }) 515 516 // run a TX using contract A 517 executionSnapshot, output, err := vm.Run( 518 context, 519 fvm.Transaction( 520 callTx("A", addressA), 521 derivedBlockData.NextTxIndexForTestingOnly()), 522 execASnapshot) 523 require.NoError(t, err) 524 require.NoError(t, output.Err) 525 526 require.Contains(t, output.Logs, "\"hello from A\"") 527 528 mainSnapshot = mainSnapshot.Append(executionSnapshot) 529 530 compareExecutionSnapshots(t, txASnapshot, executionSnapshot) 531 }) 532 533 t.Run("deploying contract C invalidates C", func(t *testing.T) { 534 require.NotNil(t, contractBSnapshot) 535 536 // deploy contract C 537 executionSnapshot, output, err := vm.Run( 538 context, 539 fvm.Transaction( 540 contractDeployTx("C", contractCCode, addressC), 541 derivedBlockData.NextTxIndexForTestingOnly()), 542 mainSnapshot) 543 require.NoError(t, err) 544 require.NoError(t, output.Err) 545 546 mainSnapshot = mainSnapshot.Append(executionSnapshot) 547 548 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 549 entryA2 := derivedBlockData.GetProgramForTestingOnly(contractA2Location) 550 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 551 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 552 553 require.NotNil(t, entryA) 554 require.NotNil(t, entryA2) 555 require.NotNil(t, entryB) 556 require.Nil(t, entryC) 557 558 cached := derivedBlockData.CachedPrograms() 559 require.Equal(t, 3, cached) 560 }) 561 562 t.Run("importing C should chain-import B and A", func(t *testing.T) { 563 executionSnapshot, output, err := vm.Run( 564 context, 565 fvm.Transaction( 566 callTx("C", addressC), 567 derivedBlockData.NextTxIndexForTestingOnly()), 568 mainSnapshot) 569 require.NoError(t, err) 570 require.NoError(t, output.Err) 571 572 require.Contains(t, output.Logs, "\"hello from C, hello from B but also hello from A\"") 573 574 mainSnapshot = mainSnapshot.Append(executionSnapshot) 575 576 // program A is the same 577 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 578 require.NotNil(t, entryA) 579 580 require.Equal(t, contractASnapshot, entryA.ExecutionSnapshot) 581 582 // program B is the same 583 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 584 require.NotNil(t, entryB) 585 586 require.Equal(t, contractBSnapshot, entryB.ExecutionSnapshot) 587 588 // program C assertions 589 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 590 require.NotNil(t, entryC) 591 592 // assert dependencies are correct 593 require.Equal(t, 4, entryC.Value.Dependencies.Count()) 594 require.NotNil(t, entryC.Value.Dependencies.ContainsLocation(contractALocation)) 595 require.NotNil(t, entryC.Value.Dependencies.ContainsLocation(contractBLocation)) 596 require.NotNil(t, entryC.Value.Dependencies.ContainsLocation(contractCLocation)) 597 598 cached := derivedBlockData.CachedPrograms() 599 require.Equal(t, 4, cached) 600 }) 601 } 602 603 func Test_ProgramsDoubleCounting(t *testing.T) { 604 snapshotTree := setupProgramsTest(t) 605 606 vm := fvm.NewVirtualMachine() 607 derivedBlockData := derived.NewEmptyDerivedBlockData(0) 608 609 metrics := &metricsReporter{} 610 context := fvm.NewContext( 611 fvm.WithContractDeploymentRestricted(false), 612 fvm.WithAuthorizationChecksEnabled(false), 613 fvm.WithSequenceNumberCheckAndIncrementEnabled(false), 614 fvm.WithCadenceLogging(true), 615 fvm.WithDerivedBlockData(derivedBlockData), 616 fvm.WithMetricsReporter(metrics)) 617 618 t.Run("deploy contracts and ensure cache is empty", func(t *testing.T) { 619 // deploy contract A 620 executionSnapshot, output, err := vm.Run( 621 context, 622 fvm.Transaction( 623 contractDeployTx("A", contractACode, addressA), 624 derivedBlockData.NextTxIndexForTestingOnly()), 625 snapshotTree) 626 require.NoError(t, err) 627 require.NoError(t, output.Err) 628 629 snapshotTree = snapshotTree.Append(executionSnapshot) 630 631 // deploy contract B 632 executionSnapshot, output, err = vm.Run( 633 context, 634 fvm.Transaction( 635 contractDeployTx("B", contractBCode, addressB), 636 derivedBlockData.NextTxIndexForTestingOnly()), 637 snapshotTree) 638 require.NoError(t, err) 639 require.NoError(t, output.Err) 640 641 snapshotTree = snapshotTree.Append(executionSnapshot) 642 643 // deploy contract C 644 executionSnapshot, output, err = vm.Run( 645 context, 646 fvm.Transaction( 647 contractDeployTx("C", contractCCode, addressC), 648 derivedBlockData.NextTxIndexForTestingOnly()), 649 snapshotTree) 650 require.NoError(t, err) 651 require.NoError(t, output.Err) 652 653 snapshotTree = snapshotTree.Append(executionSnapshot) 654 655 // deploy contract A2 last to clear any cache so far 656 executionSnapshot, output, err = vm.Run( 657 context, 658 fvm.Transaction( 659 contractDeployTx("A2", contractA2Code, addressA), 660 derivedBlockData.NextTxIndexForTestingOnly()), 661 snapshotTree) 662 require.NoError(t, err) 663 require.NoError(t, output.Err) 664 665 snapshotTree = snapshotTree.Append(executionSnapshot) 666 667 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 668 entryA2 := derivedBlockData.GetProgramForTestingOnly(contractA2Location) 669 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 670 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 671 672 require.Nil(t, entryA) 673 require.Nil(t, entryA2) 674 require.Nil(t, entryB) 675 require.Nil(t, entryC) 676 677 cached := derivedBlockData.CachedPrograms() 678 require.Equal(t, 0, cached) 679 }) 680 681 callC := func(snapshotTree snapshot.SnapshotTree) snapshot.SnapshotTree { 682 procCallC := fvm.Transaction( 683 flow.NewTransactionBody().SetScript( 684 []byte( 685 ` 686 import A from 0xa 687 import B from 0xb 688 import C from 0xc 689 transaction { 690 prepare() { 691 log(C.hello()) 692 } 693 }`, 694 )), 695 derivedBlockData.NextTxIndexForTestingOnly()) 696 697 executionSnapshot, output, err := vm.Run( 698 context, 699 procCallC, 700 snapshotTree) 701 require.NoError(t, err) 702 require.NoError(t, output.Err) 703 704 require.Equal( 705 t, 706 uint( 707 1+ // import A 708 3+ // import B (import A, import A2) 709 4, // import C (import B (3), import A (already imported in this scope)) 710 ), 711 output.ComputationIntensities[environment.ComputationKindGetCode]) 712 713 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 714 entryA2 := derivedBlockData.GetProgramForTestingOnly(contractA2Location) 715 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 716 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 717 718 require.NotNil(t, entryA) 719 require.NotNil(t, entryA2) // loaded due to "*" import 720 require.NotNil(t, entryB) 721 require.NotNil(t, entryC) 722 723 cached := derivedBlockData.CachedPrograms() 724 require.Equal(t, 4, cached) 725 726 return snapshotTree.Append(executionSnapshot) 727 } 728 729 t.Run("Call C", func(t *testing.T) { 730 metrics.Reset() 731 snapshotTree = callC(snapshotTree) 732 733 // miss A because loading transaction 734 // hit A because loading B because loading transaction 735 // miss A2 because loading B because loading transaction 736 // miss B because loading transaction 737 // hit B because loading C because loading transaction 738 // hit A because loading C because loading transaction 739 // miss C because loading transaction 740 // 741 // hit C because interpreting transaction 742 // hit B because interpreting C because interpreting transaction 743 // hit A because interpreting B because interpreting C because interpreting transaction 744 // hit A2 because interpreting B because interpreting C because interpreting transaction 745 require.Equal(t, 7, metrics.CacheHits) 746 require.Equal(t, 4, metrics.CacheMisses) 747 }) 748 749 t.Run("Call C Again", func(t *testing.T) { 750 metrics.Reset() 751 snapshotTree = callC(snapshotTree) 752 753 // hit A because loading transaction 754 // hit B because loading transaction 755 // hit C because loading transaction 756 // 757 // hit C because interpreting transaction 758 // hit B because interpreting C because interpreting transaction 759 // hit A because interpreting B because interpreting C because interpreting transaction 760 // hit A2 because interpreting B because interpreting C because interpreting transaction 761 require.Equal(t, 7, metrics.CacheHits) 762 require.Equal(t, 0, metrics.CacheMisses) 763 }) 764 765 t.Run("update A to breaking change and ensure cache state", func(t *testing.T) { 766 // deploy contract A 767 executionSnapshot, output, err := vm.Run( 768 context, 769 fvm.Transaction( 770 updateContractTx("A", contractABreakingCode, addressA), 771 derivedBlockData.NextTxIndexForTestingOnly()), 772 snapshotTree) 773 require.NoError(t, err) 774 require.NoError(t, output.Err) 775 776 snapshotTree = snapshotTree.Append(executionSnapshot) 777 778 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 779 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 780 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 781 782 require.Nil(t, entryA) 783 require.Nil(t, entryB) 784 require.Nil(t, entryC) 785 786 cached := derivedBlockData.CachedPrograms() 787 require.Equal(t, 1, cached) 788 }) 789 790 callCAfterItsBroken := func(snapshotTree snapshot.SnapshotTree) snapshot.SnapshotTree { 791 procCallC := fvm.Transaction( 792 flow.NewTransactionBody().SetScript( 793 []byte( 794 ` 795 import A from 0xa 796 import B from 0xb 797 import C from 0xc 798 transaction { 799 prepare() { 800 log(C.hello()) 801 } 802 }`, 803 )), 804 derivedBlockData.NextTxIndexForTestingOnly()) 805 806 executionSnapshot, output, err := vm.Run( 807 context, 808 procCallC, 809 snapshotTree) 810 require.NoError(t, err) 811 require.Error(t, output.Err) 812 813 entryA := derivedBlockData.GetProgramForTestingOnly(contractALocation) 814 entryA2 := derivedBlockData.GetProgramForTestingOnly(contractA2Location) 815 entryB := derivedBlockData.GetProgramForTestingOnly(contractBLocation) 816 entryC := derivedBlockData.GetProgramForTestingOnly(contractCLocation) 817 818 require.NotNil(t, entryA) 819 require.NotNil(t, entryA2) // loaded due to "*" import in B 820 require.Nil(t, entryB) // failed to load 821 require.Nil(t, entryC) // failed to load 822 823 cached := derivedBlockData.CachedPrograms() 824 require.Equal(t, 2, cached) 825 826 return snapshotTree.Append(executionSnapshot) 827 } 828 829 t.Run("Call C when broken", func(t *testing.T) { 830 metrics.Reset() 831 snapshotTree = callCAfterItsBroken(snapshotTree) 832 833 // miss A, hit A, hit A2, hit A, hit A2, hit A 834 require.Equal(t, 5, metrics.CacheHits) 835 require.Equal(t, 1, metrics.CacheMisses) 836 }) 837 838 } 839 840 func callTx(name string, address flow.Address) *flow.TransactionBody { 841 842 return flow.NewTransactionBody().SetScript( 843 []byte(fmt.Sprintf(` 844 import %s from %s 845 transaction { 846 prepare() { 847 log(%s.hello()) 848 } 849 }`, name, address.HexWithPrefix(), name)), 850 ) 851 } 852 853 func contractDeployTx(name, code string, address flow.Address) *flow.TransactionBody { 854 encoded := hex.EncodeToString([]byte(code)) 855 856 return flow.NewTransactionBody().SetScript( 857 []byte(fmt.Sprintf(`transaction { 858 prepare(signer: auth(AddContract) &Account) { 859 signer.contracts.add(name: "%s", code: "%s".decodeHex()) 860 } 861 }`, name, encoded)), 862 ).AddAuthorizer(address) 863 } 864 865 func updateContractTx(name, code string, address flow.Address) *flow.TransactionBody { 866 encoded := hex.EncodeToString([]byte(code)) 867 868 return flow.NewTransactionBody().SetScript([]byte( 869 fmt.Sprintf(`transaction { 870 prepare(signer: auth(UpdateContract) &Account) { 871 signer.contracts.update(name: "%s", code: "%s".decodeHex()) 872 } 873 }`, name, encoded)), 874 ).AddAuthorizer(address) 875 } 876 877 func compareExecutionSnapshots(t *testing.T, a, b *snapshot.ExecutionSnapshot) { 878 require.Equal(t, a.WriteSet, b.WriteSet) 879 require.Equal(t, a.ReadSet, b.ReadSet) 880 require.Equal(t, a.SpockSecret, b.SpockSecret) 881 } 882 883 type metricsReporter struct { 884 CacheHits int 885 CacheMisses int 886 } 887 888 func (m *metricsReporter) RuntimeTransactionParsed(duration time.Duration) {} 889 890 func (m *metricsReporter) RuntimeTransactionChecked(duration time.Duration) {} 891 892 func (m *metricsReporter) RuntimeTransactionInterpreted(duration time.Duration) {} 893 894 func (m *metricsReporter) RuntimeSetNumberOfAccounts(count uint64) {} 895 896 func (m *metricsReporter) RuntimeTransactionProgramsCacheMiss() { 897 m.CacheMisses++ 898 } 899 900 func (m *metricsReporter) RuntimeTransactionProgramsCacheHit() { 901 m.CacheHits++ 902 } 903 904 func (m *metricsReporter) Reset() { 905 m.CacheHits = 0 906 m.CacheMisses = 0 907 } 908 909 var _ environment.MetricsReporter = (*metricsReporter)(nil)