github.com/koko1123/flow-go-1@v0.29.6/fvm/fvm_bench_test.go (about) 1 package fvm_test 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "math/rand" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/ipfs/go-datastore" 14 dssync "github.com/ipfs/go-datastore/sync" 15 blockstore "github.com/ipfs/go-ipfs-blockstore" 16 "github.com/rs/zerolog" 17 "github.com/stretchr/testify/mock" 18 "github.com/stretchr/testify/require" 19 20 "github.com/onflow/cadence" 21 jsoncdc "github.com/onflow/cadence/encoding/json" 22 "github.com/onflow/cadence/runtime" 23 24 flow2 "github.com/onflow/flow-go-sdk" 25 "github.com/onflow/flow-go-sdk/templates" 26 27 "github.com/koko1123/flow-go-1/engine/execution" 28 "github.com/koko1123/flow-go-1/engine/execution/computation" 29 "github.com/koko1123/flow-go-1/engine/execution/computation/committer" 30 "github.com/koko1123/flow-go-1/engine/execution/computation/computer" 31 exeState "github.com/koko1123/flow-go-1/engine/execution/state" 32 bootstrapexec "github.com/koko1123/flow-go-1/engine/execution/state/bootstrap" 33 "github.com/koko1123/flow-go-1/engine/execution/state/delta" 34 "github.com/koko1123/flow-go-1/engine/execution/testutil" 35 "github.com/koko1123/flow-go-1/fvm" 36 "github.com/koko1123/flow-go-1/fvm/derived" 37 reusableRuntime "github.com/koko1123/flow-go-1/fvm/runtime" 38 "github.com/koko1123/flow-go-1/fvm/state" 39 completeLedger "github.com/koko1123/flow-go-1/ledger/complete" 40 "github.com/koko1123/flow-go-1/ledger/complete/wal/fixtures" 41 "github.com/koko1123/flow-go-1/model/flow" 42 "github.com/koko1123/flow-go-1/module/executiondatasync/execution_data" 43 "github.com/koko1123/flow-go-1/module/executiondatasync/provider" 44 mocktracker "github.com/koko1123/flow-go-1/module/executiondatasync/tracker/mock" 45 "github.com/koko1123/flow-go-1/module/metrics" 46 moduleMock "github.com/koko1123/flow-go-1/module/mock" 47 requesterunit "github.com/koko1123/flow-go-1/module/state_synchronization/requester/unittest" 48 "github.com/koko1123/flow-go-1/module/trace" 49 "github.com/koko1123/flow-go-1/utils/unittest" 50 ) 51 52 // TODO (ramtin) 53 // - move block computer logic to its own location inside exec node, so we embed a real 54 // block computer, simplify this one to use an in-mem ledger 55 // - time track different part of execution (execution, ledger update, ...) 56 // - add metrics like number of registers touched, proof size 57 // 58 59 type TestBenchBlockExecutor interface { 60 ExecuteCollections(tb testing.TB, collections [][]*flow.TransactionBody) *execution.ComputationResult 61 Chain(tb testing.TB) flow.Chain 62 ServiceAccount(tb testing.TB) *TestBenchAccount 63 } 64 65 type TestBenchAccount struct { 66 SeqNumber uint64 67 PrivateKey flow.AccountPrivateKey 68 Address flow.Address 69 } 70 71 func (account *TestBenchAccount) RetAndIncSeqNumber() uint64 { 72 account.SeqNumber++ 73 return account.SeqNumber - 1 74 } 75 76 func (account *TestBenchAccount) DeployContract(b *testing.B, blockExec TestBenchBlockExecutor, contractName string, contract string) { 77 serviceAccount := blockExec.ServiceAccount(b) 78 txBody := testutil.CreateContractDeploymentTransaction( 79 contractName, 80 contract, 81 account.Address, 82 blockExec.Chain(b)) 83 84 txBody.SetProposalKey(serviceAccount.Address, 0, serviceAccount.RetAndIncSeqNumber()) 85 txBody.SetPayer(serviceAccount.Address) 86 87 err := testutil.SignPayload(txBody, account.Address, account.PrivateKey) 88 require.NoError(b, err) 89 90 err = testutil.SignEnvelope(txBody, serviceAccount.Address, serviceAccount.PrivateKey) 91 require.NoError(b, err) 92 93 computationResult := blockExec.ExecuteCollections(b, [][]*flow.TransactionBody{{txBody}}) 94 require.Empty(b, computationResult.TransactionResults[0].ErrorMessage) 95 } 96 97 func (account *TestBenchAccount) AddArrayToStorage(b *testing.B, blockExec TestBenchBlockExecutor, list []string) { 98 serviceAccount := blockExec.ServiceAccount(b) 99 txBody := flow.NewTransactionBody(). 100 SetScript([]byte(` 101 transaction(list: [String]) { 102 prepare(acct: AuthAccount) { 103 acct.load<[String]>(from: /storage/test) 104 acct.save(list, to: /storage/test) 105 } 106 execute {} 107 } 108 `)). 109 AddAuthorizer(account.Address) 110 111 cadenceArrayValues := make([]cadence.Value, len(list)) 112 for i, item := range list { 113 cadenceArrayValues[i] = cadence.String(item) 114 } 115 cadenceArray, err := jsoncdc.Encode(cadence.NewArray(cadenceArrayValues)) 116 require.NoError(b, err) 117 txBody.AddArgument(cadenceArray) 118 119 txBody.SetProposalKey(serviceAccount.Address, 0, serviceAccount.RetAndIncSeqNumber()) 120 txBody.SetPayer(serviceAccount.Address) 121 122 if account.Address != serviceAccount.Address { 123 err = testutil.SignPayload(txBody, account.Address, account.PrivateKey) 124 require.NoError(b, err) 125 } 126 127 err = testutil.SignEnvelope(txBody, serviceAccount.Address, serviceAccount.PrivateKey) 128 require.NoError(b, err) 129 130 computationResult := blockExec.ExecuteCollections(b, [][]*flow.TransactionBody{{txBody}}) 131 require.Empty(b, computationResult.TransactionResults[0].ErrorMessage) 132 } 133 134 // BasicBlockExecutor executes blocks in sequence and applies all changes (not fork aware) 135 type BasicBlockExecutor struct { 136 blockComputer computer.BlockComputer 137 derivedChainData *derived.DerivedChainData 138 activeView state.View 139 activeStateCommitment flow.StateCommitment 140 chain flow.Chain 141 serviceAccount *TestBenchAccount 142 onStopFunc func() 143 } 144 145 func NewBasicBlockExecutor(tb testing.TB, chain flow.Chain, logger zerolog.Logger) *BasicBlockExecutor { 146 vm := fvm.NewVirtualMachine() 147 148 // a big interaction limit since that is not what's being tested. 149 interactionLimit := fvm.DefaultMaxInteractionSize * uint64(1000) 150 151 opts := []fvm.Option{ 152 fvm.WithTransactionFeesEnabled(true), 153 fvm.WithAccountStorageLimit(true), 154 fvm.WithChain(chain), 155 fvm.WithLogger(logger), 156 fvm.WithMaxStateInteractionSize(interactionLimit), 157 fvm.WithReusableCadenceRuntimePool( 158 reusableRuntime.NewReusableCadenceRuntimePool( 159 computation.ReusableCadenceRuntimePoolSize, 160 runtime.Config{}, 161 ), 162 ), 163 } 164 fvmContext := fvm.NewContext(opts...) 165 166 collector := metrics.NewNoopCollector() 167 tracer := trace.NewNoopTracer() 168 169 wal := &fixtures.NoopWAL{} 170 171 ledger, err := completeLedger.NewLedger(wal, 100, collector, logger, completeLedger.DefaultPathFinderVersion) 172 require.NoError(tb, err) 173 174 compactor := fixtures.NewNoopCompactor(ledger) 175 <-compactor.Ready() 176 177 onStopFunc := func() { 178 <-ledger.Done() 179 <-compactor.Done() 180 } 181 182 bootstrapper := bootstrapexec.NewBootstrapper(logger) 183 184 serviceAccount := &TestBenchAccount{ 185 SeqNumber: 0, 186 PrivateKey: unittest.ServiceAccountPrivateKey, 187 Address: chain.ServiceAddress(), 188 } 189 190 initialCommit, err := bootstrapper.BootstrapLedger( 191 ledger, 192 unittest.ServiceAccountPublicKey, 193 chain, 194 fvm.WithInitialTokenSupply(unittest.GenesisTokenSupply), 195 fvm.WithAccountCreationFee(fvm.DefaultAccountCreationFee), 196 fvm.WithMinimumStorageReservation(fvm.DefaultMinimumStorageReservation), 197 fvm.WithTransactionFee(fvm.DefaultTransactionFees), 198 fvm.WithStorageMBPerFLOW(fvm.DefaultStorageMBPerFLOW), 199 ) 200 require.NoError(tb, err) 201 202 bservice := requesterunit.MockBlobService(blockstore.NewBlockstore(dssync.MutexWrap(datastore.NewMapDatastore()))) 203 trackerStorage := mocktracker.NewMockStorage() 204 205 prov := provider.NewProvider( 206 zerolog.Nop(), 207 metrics.NewNoopCollector(), 208 execution_data.DefaultSerializer, 209 bservice, 210 trackerStorage, 211 ) 212 213 me := new(moduleMock.Local) 214 me.On("SignFunc", mock.Anything, mock.Anything, mock.Anything). 215 Return(nil, nil) 216 217 ledgerCommitter := committer.NewLedgerViewCommitter(ledger, tracer) 218 blockComputer, err := computer.NewBlockComputer( 219 vm, 220 fvmContext, 221 collector, 222 tracer, 223 logger, 224 ledgerCommitter, 225 me, 226 prov) 227 require.NoError(tb, err) 228 229 view := delta.NewView(exeState.LedgerGetRegister(ledger, initialCommit)) 230 231 derivedChainData, err := derived.NewDerivedChainData( 232 derived.DefaultDerivedDataCacheSize) 233 require.NoError(tb, err) 234 235 return &BasicBlockExecutor{ 236 blockComputer: blockComputer, 237 derivedChainData: derivedChainData, 238 activeStateCommitment: initialCommit, 239 activeView: view, 240 chain: chain, 241 serviceAccount: serviceAccount, 242 onStopFunc: onStopFunc, 243 } 244 } 245 246 func (b *BasicBlockExecutor) Chain(_ testing.TB) flow.Chain { 247 return b.chain 248 } 249 250 func (b *BasicBlockExecutor) ServiceAccount(_ testing.TB) *TestBenchAccount { 251 return b.serviceAccount 252 } 253 254 func (b *BasicBlockExecutor) ExecuteCollections(tb testing.TB, collections [][]*flow.TransactionBody) *execution.ComputationResult { 255 executableBlock := unittest.ExecutableBlockFromTransactions(b.chain.ChainID(), collections) 256 executableBlock.StartState = &b.activeStateCommitment 257 258 derivedBlockData := b.derivedChainData.GetOrCreateDerivedBlockData( 259 executableBlock.ID(), 260 executableBlock.ParentID()) 261 262 computationResult, err := b.blockComputer.ExecuteBlock(context.Background(), executableBlock, b.activeView, derivedBlockData) 263 require.NoError(tb, err) 264 265 endState, _, _, err := execution.GenerateExecutionResultAndChunkDataPacks(metrics.NewNoopCollector(), unittest.IdentifierFixture(), b.activeStateCommitment, computationResult) 266 require.NoError(tb, err) 267 b.activeStateCommitment = endState 268 269 return computationResult 270 } 271 272 func (b *BasicBlockExecutor) SetupAccounts(tb testing.TB, privateKeys []flow.AccountPrivateKey) []TestBenchAccount { 273 accounts := make([]TestBenchAccount, 0) 274 serviceAddress := b.Chain(tb).ServiceAddress() 275 276 for _, privateKey := range privateKeys { 277 accountKey := flow2.NewAccountKey(). 278 FromPrivateKey(privateKey.PrivateKey). 279 SetWeight(fvm.AccountKeyWeightThreshold). 280 SetHashAlgo(privateKey.HashAlgo). 281 SetSigAlgo(privateKey.SignAlgo) 282 283 sdkTX, err := templates.CreateAccount([]*flow2.AccountKey{accountKey}, []templates.Contract{}, flow2.BytesToAddress(serviceAddress.Bytes())) 284 require.NoError(tb, err) 285 286 txBody := flow.NewTransactionBody(). 287 SetScript(sdkTX.Script). 288 SetArguments(sdkTX.Arguments). 289 AddAuthorizer(serviceAddress). 290 SetProposalKey(serviceAddress, 0, b.ServiceAccount(tb).RetAndIncSeqNumber()). 291 SetPayer(serviceAddress) 292 293 err = testutil.SignEnvelope(txBody, b.Chain(tb).ServiceAddress(), unittest.ServiceAccountPrivateKey) 294 require.NoError(tb, err) 295 296 computationResult := b.ExecuteCollections(tb, [][]*flow.TransactionBody{{txBody}}) 297 require.Empty(tb, computationResult.TransactionResults[0].ErrorMessage) 298 299 var addr flow.Address 300 301 for _, eventList := range computationResult.Events { 302 for _, event := range eventList { 303 if event.Type == flow.EventAccountCreated { 304 data, err := jsoncdc.Decode(nil, event.Payload) 305 if err != nil { 306 tb.Fatal("setup account failed, error decoding events") 307 } 308 addr = flow.Address(data.(cadence.Event).Fields[0].(cadence.Address)) 309 break 310 } 311 } 312 } 313 if addr == flow.EmptyAddress { 314 tb.Fatal("setup account failed, no account creation event emitted") 315 } 316 accounts = append(accounts, TestBenchAccount{Address: addr, PrivateKey: privateKey, SeqNumber: 0}) 317 } 318 319 return accounts 320 } 321 322 type logExtractor struct { 323 TimeSpent map[string]uint64 324 InteractionUsed map[string]uint64 325 } 326 327 type txWeights struct { 328 TXHash string `json:"tx_id"` 329 LedgerInteractionUsed uint64 `json:"ledgerInteractionUsed"` 330 ComputationUsed uint `json:"computationUsed"` 331 MemoryEstimate uint `json:"memoryEstimate"` 332 } 333 334 type txSuccessfulLog struct { 335 TxID string `json:"tx_id"` 336 TimeSpentInMS uint64 `json:"timeSpentInMS"` 337 } 338 339 func (l *logExtractor) Write(p []byte) (n int, err error) { 340 if strings.Contains(string(p), "transaction execution data") { 341 w := txWeights{} 342 err := json.Unmarshal(p, &w) 343 344 if err != nil { 345 fmt.Println(err) 346 return len(p), nil 347 } 348 349 l.InteractionUsed[w.TXHash] = w.LedgerInteractionUsed 350 } 351 if strings.Contains(string(p), "transaction executed successfully") { 352 w := txSuccessfulLog{} 353 err := json.Unmarshal(p, &w) 354 355 if err != nil { 356 fmt.Println(err) 357 return len(p), nil 358 } 359 l.TimeSpent[w.TxID] = w.TimeSpentInMS 360 } 361 return len(p), nil 362 363 } 364 365 var _ io.Writer = &logExtractor{} 366 367 // BenchmarkRuntimeEmptyTransaction simulates executing blocks with `transactionsPerBlock` 368 // where each transaction is an empty transaction 369 func BenchmarkRuntimeTransaction(b *testing.B) { 370 rand.Seed(time.Now().UnixNano()) 371 372 transactionsPerBlock := 10 373 374 longString := strings.Repeat("0", 1000) 375 376 chain := flow.Testnet.Chain() 377 378 logE := &logExtractor{ 379 TimeSpent: map[string]uint64{}, 380 InteractionUsed: map[string]uint64{}, 381 } 382 383 benchTransaction := func(b *testing.B, tx string) { 384 385 logger := zerolog.New(logE).Level(zerolog.DebugLevel) 386 387 blockExecutor := NewBasicBlockExecutor(b, chain, logger) 388 defer func() { 389 blockExecutor.onStopFunc() 390 }() 391 392 // Create an account private key. 393 privateKeys, err := testutil.GenerateAccountPrivateKeys(1) 394 require.NoError(b, err) 395 396 accounts := blockExecutor.SetupAccounts(b, privateKeys) 397 398 addrs := []flow.Address{} 399 for _, account := range accounts { 400 addrs = append(addrs, account.Address) 401 } 402 // fund all accounts so not to run into storage problems 403 fundAccounts(b, blockExecutor, cadence.UFix64(10_000_000_000), addrs...) 404 405 accounts[0].DeployContract(b, blockExecutor, "TestContract", ` 406 access(all) contract TestContract { 407 access(all) event SomeEvent() 408 409 access(all) fun empty() { 410 } 411 412 access(all) fun emit() { 413 emit SomeEvent() 414 } 415 } 416 `) 417 418 accounts[0].AddArrayToStorage(b, blockExecutor, []string{longString, longString, longString, longString, longString}) 419 420 btx := []byte(tx) 421 422 benchmarkAccount := &accounts[0] 423 424 b.ResetTimer() // setup done, lets start measuring 425 for i := 0; i < b.N; i++ { 426 transactions := make([]*flow.TransactionBody, transactionsPerBlock) 427 for j := 0; j < transactionsPerBlock; j++ { 428 txBody := flow.NewTransactionBody(). 429 SetScript(btx). 430 AddAuthorizer(benchmarkAccount.Address). 431 SetProposalKey(benchmarkAccount.Address, 0, benchmarkAccount.RetAndIncSeqNumber()). 432 SetPayer(benchmarkAccount.Address) 433 434 err = testutil.SignEnvelope(txBody, benchmarkAccount.Address, benchmarkAccount.PrivateKey) 435 require.NoError(b, err) 436 437 transactions[j] = txBody 438 } 439 440 computationResult := blockExecutor.ExecuteCollections(b, [][]*flow.TransactionBody{transactions}) 441 totalInteractionUsed := uint64(0) 442 totalComputationUsed := uint64(0) 443 for j := 0; j < transactionsPerBlock; j++ { 444 require.Empty(b, computationResult.TransactionResults[j].ErrorMessage) 445 totalInteractionUsed += logE.InteractionUsed[computationResult.TransactionResults[j].ID().String()] 446 totalComputationUsed += computationResult.TransactionResults[j].ComputationUsed 447 } 448 b.ReportMetric(float64(totalInteractionUsed/uint64(transactionsPerBlock)), "interactions") 449 b.ReportMetric(float64(totalComputationUsed/uint64(transactionsPerBlock)), "computation") 450 } 451 } 452 453 templateTx := func(rep int, prepare string) string { 454 return fmt.Sprintf(` 455 import FungibleToken from 0x%s 456 import FlowToken from 0x%s 457 import TestContract from 0x%s 458 459 transaction(){ 460 prepare(signer: AuthAccount){ 461 var i = 0 462 while i < %d { 463 i = i + 1 464 %s 465 } 466 } 467 }`, fvm.FungibleTokenAddress(chain), fvm.FlowTokenAddress(chain), "754aed9de6197641", rep, prepare) 468 } 469 470 b.Run("reference tx", func(b *testing.B) { 471 benchTransaction(b, templateTx(100, "")) 472 }) 473 b.Run("convert int to string", func(b *testing.B) { 474 benchTransaction(b, templateTx(100, `i.toString()`)) 475 }) 476 b.Run("convert int to string and concatenate it", func(b *testing.B) { 477 benchTransaction(b, templateTx(100, `"x".concat(i.toString())`)) 478 }) 479 b.Run("get signer address", func(b *testing.B) { 480 benchTransaction(b, templateTx(100, `signer.address`)) 481 }) 482 b.Run("get public account", func(b *testing.B) { 483 benchTransaction(b, templateTx(100, `getAccount(signer.address)`)) 484 }) 485 b.Run("get account and get balance", func(b *testing.B) { 486 benchTransaction(b, templateTx(100, `getAccount(signer.address).balance`)) 487 }) 488 b.Run("get account and get available balance", func(b *testing.B) { 489 benchTransaction(b, templateTx(100, `getAccount(signer.address).availableBalance`)) 490 }) 491 b.Run("get account and get storage used", func(b *testing.B) { 492 benchTransaction(b, templateTx(100, `getAccount(signer.address).storageUsed`)) 493 }) 494 b.Run("get account and get storage capacity", func(b *testing.B) { 495 benchTransaction(b, templateTx(100, `getAccount(signer.address).storageCapacity`)) 496 }) 497 b.Run("get signer vault", func(b *testing.B) { 498 benchTransaction( 499 b, 500 templateTx(100, `let vaultRef = signer.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)!`), 501 ) 502 }) 503 b.Run("get signer receiver", func(b *testing.B) { 504 benchTransaction( 505 b, 506 templateTx(100, `let receiverRef = getAccount(signer.address) 507 .getCapability(/public/flowTokenReceiver) 508 .borrow<&{FungibleToken.Receiver}>()!`), 509 ) 510 }) 511 b.Run("transfer tokens", func(b *testing.B) { 512 benchTransaction( 513 b, 514 templateTx(100, ` 515 let receiverRef = getAccount(signer.address) 516 .getCapability(/public/flowTokenReceiver) 517 .borrow<&{FungibleToken.Receiver}>()! 518 519 let vaultRef = signer.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)! 520 521 receiverRef.deposit(from: <-vaultRef.withdraw(amount: 0.00001)) 522 `), 523 ) 524 }) 525 b.Run("load and save empty string on signers address", func(b *testing.B) { 526 benchTransaction( 527 b, 528 templateTx(100, ` 529 signer.load<String>(from: /storage/testpath) 530 signer.save("", to: /storage/testpath) 531 `), 532 ) 533 }) 534 b.Run("load and save long string on signers address", func(b *testing.B) { 535 benchTransaction( 536 b, 537 templateTx(100, fmt.Sprintf(` 538 signer.load<String>(from: /storage/testpath) 539 signer.save("%s", to: /storage/testpath) 540 `, longString)), 541 ) 542 }) 543 b.Run("create new account", func(b *testing.B) { 544 benchTransaction(b, templateTx(50, `let acct = AuthAccount(payer: signer)`)) 545 }) 546 b.Run("call empty contract function", func(b *testing.B) { 547 benchTransaction(b, templateTx(100, `TestContract.empty()`)) 548 }) 549 b.Run("emit event", func(b *testing.B) { 550 benchTransaction(b, templateTx(100, `TestContract.emit()`)) 551 }) 552 b.Run("borrow array from storage", func(b *testing.B) { 553 benchTransaction( 554 b, 555 templateTx(100, ` 556 let strings = signer.borrow<&[String]>(from: /storage/test)! 557 var i = 0 558 while (i < strings.length) { 559 log(strings[i]) 560 i = i +1 561 } 562 `), 563 ) 564 }) 565 b.Run("copy array from storage", func(b *testing.B) { 566 benchTransaction( 567 b, 568 templateTx(100, ` 569 let strings = signer.copy<[String]>(from: /storage/test)! 570 var i = 0 571 while (i < strings.length) { 572 log(strings[i]) 573 i = i +1 574 } 575 `), 576 ) 577 }) 578 } 579 580 const TransferTxTemplate = ` 581 import NonFungibleToken from 0x%s 582 import BatchNFT from 0x%s 583 584 transaction(testTokenIDs: [UInt64], recipientAddress: Address) { 585 let transferTokens: @NonFungibleToken.Collection 586 587 prepare(acct: AuthAccount) { 588 let ref = acct.borrow<&BatchNFT.Collection>(from: /storage/TestTokenCollection)! 589 self.transferTokens <- ref.batchWithdraw(ids: testTokenIDs) 590 } 591 592 execute { 593 // get the recipient's public account object 594 let recipient = getAccount(recipientAddress) 595 // get the Collection reference for the receiver 596 let receiverRef = recipient.getCapability(/public/TestTokenCollection) 597 .borrow<&{BatchNFT.TestTokenCollectionPublic}>()! 598 // deposit the NFT in the receivers collection 599 receiverRef.batchDeposit(tokens: <-self.transferTokens) 600 } 601 }` 602 603 // BenchmarkRuntimeNFTBatchTransfer runs BenchRunNFTBatchTransfer with BasicBlockExecutor 604 func BenchmarkRuntimeNFTBatchTransfer(b *testing.B) { 605 blockExecutor := NewBasicBlockExecutor(b, flow.Testnet.Chain(), zerolog.Nop()) 606 defer func() { 607 blockExecutor.onStopFunc() 608 }() 609 610 // Create an account private key. 611 privateKeys, err := testutil.GenerateAccountPrivateKeys(3) 612 require.NoError(b, err) 613 614 // Bootstrap a ledger, creating accounts with the provided private keys and the root account. 615 accounts := blockExecutor.SetupAccounts(b, privateKeys) 616 617 BenchRunNFTBatchTransfer(b, blockExecutor, accounts) 618 } 619 620 // BenchRunNFTBatchTransfer simulates executing blocks with `transactionsPerBlock` 621 // where each transaction transfers `testTokensPerTransaction` testTokens (NFTs) 622 func BenchRunNFTBatchTransfer(b *testing.B, 623 blockExecutor TestBenchBlockExecutor, 624 accounts []TestBenchAccount) { 625 626 transactionsPerBlock := 10 627 testTokensPerTransaction := 10 628 629 serviceAccount := blockExecutor.ServiceAccount(b) 630 // deploy NFT 631 nftAccount := accounts[0] 632 deployNFT(b, blockExecutor, &nftAccount) 633 634 // deploy NFT 635 batchNFTAccount := accounts[1] 636 deployBatchNFT(b, blockExecutor, &batchNFTAccount, nftAccount.Address) 637 638 // fund all accounts so not to run into storage problems 639 fundAccounts(b, blockExecutor, cadence.UFix64(10_0000_0000), nftAccount.Address, batchNFTAccount.Address, accounts[2].Address) 640 641 // mint nfts into the batchNFT account 642 mintNFTs(b, blockExecutor, &accounts[1], transactionsPerBlock*testTokensPerTransaction*b.N) 643 644 // set up receiver 645 setupReceiver(b, blockExecutor, &nftAccount, &batchNFTAccount, &accounts[2]) 646 647 // Transfer NFTs 648 transferTx := []byte(fmt.Sprintf(TransferTxTemplate, accounts[0].Address.Hex(), accounts[1].Address.Hex())) 649 650 encodedAddress, err := jsoncdc.Encode(cadence.Address(accounts[2].Address)) 651 require.NoError(b, err) 652 653 var computationResult *execution.ComputationResult 654 655 b.ResetTimer() // setup done, lets start measuring 656 for i := 0; i < b.N; i++ { 657 transactions := make([]*flow.TransactionBody, transactionsPerBlock) 658 for j := 0; j < transactionsPerBlock; j++ { 659 cadenceValues := make([]cadence.Value, testTokensPerTransaction) 660 startTestToken := (i*transactionsPerBlock+j)*testTokensPerTransaction + 1 661 for m := 0; m < testTokensPerTransaction; m++ { 662 cadenceValues[m] = cadence.NewUInt64(uint64(startTestToken + m)) 663 } 664 665 encodedArg, err := jsoncdc.Encode( 666 cadence.NewArray(cadenceValues), 667 ) 668 require.NoError(b, err) 669 670 txBody := flow.NewTransactionBody(). 671 SetScript(transferTx). 672 SetProposalKey(serviceAccount.Address, 0, serviceAccount.RetAndIncSeqNumber()). 673 AddAuthorizer(accounts[1].Address). 674 AddArgument(encodedArg). 675 AddArgument(encodedAddress). 676 SetPayer(serviceAccount.Address) 677 678 err = testutil.SignPayload(txBody, accounts[1].Address, accounts[1].PrivateKey) 679 require.NoError(b, err) 680 681 err = testutil.SignEnvelope(txBody, serviceAccount.Address, serviceAccount.PrivateKey) 682 require.NoError(b, err) 683 684 transactions[j] = txBody 685 } 686 687 computationResult = blockExecutor.ExecuteCollections(b, [][]*flow.TransactionBody{transactions}) 688 for j := 0; j < transactionsPerBlock; j++ { 689 require.Empty(b, computationResult.TransactionResults[j].ErrorMessage) 690 } 691 } 692 } 693 694 func setupReceiver(b *testing.B, be TestBenchBlockExecutor, nftAccount, batchNFTAccount, targetAccount *TestBenchAccount) { 695 serviceAccount := be.ServiceAccount(b) 696 697 setUpReceiverTemplate := ` 698 import NonFungibleToken from 0x%s 699 import BatchNFT from 0x%s 700 701 transaction { 702 prepare(signer: AuthAccount) { 703 signer.save( 704 <-BatchNFT.createEmptyCollection(), 705 to: /storage/TestTokenCollection 706 ) 707 signer.link<&BatchNFT.Collection>( 708 /public/TestTokenCollection, 709 target: /storage/TestTokenCollection 710 ) 711 } 712 }` 713 714 setupTx := []byte(fmt.Sprintf(setUpReceiverTemplate, nftAccount.Address.Hex(), batchNFTAccount.Address.Hex())) 715 716 txBody := flow.NewTransactionBody(). 717 SetScript(setupTx). 718 SetProposalKey(serviceAccount.Address, 0, serviceAccount.RetAndIncSeqNumber()). 719 AddAuthorizer(targetAccount.Address). 720 SetPayer(serviceAccount.Address) 721 722 err := testutil.SignPayload(txBody, targetAccount.Address, targetAccount.PrivateKey) 723 require.NoError(b, err) 724 725 err = testutil.SignEnvelope(txBody, serviceAccount.Address, serviceAccount.PrivateKey) 726 require.NoError(b, err) 727 728 computationResult := be.ExecuteCollections(b, [][]*flow.TransactionBody{{txBody}}) 729 require.Empty(b, computationResult.TransactionResults[0].ErrorMessage) 730 } 731 732 func mintNFTs(b *testing.B, be TestBenchBlockExecutor, batchNFTAccount *TestBenchAccount, size int) { 733 serviceAccount := be.ServiceAccount(b) 734 mintScriptTemplate := ` 735 import BatchNFT from 0x%s 736 transaction { 737 prepare(signer: AuthAccount) { 738 let adminRef = signer.borrow<&BatchNFT.Admin>(from: /storage/BatchNFTAdmin)! 739 let playID = adminRef.createPlay(metadata: {"name": "Test"}) 740 let setID = BatchNFT.nextSetID 741 adminRef.createSet(name: "Test") 742 let setRef = adminRef.borrowSet(setID: setID) 743 setRef.addPlay(playID: playID) 744 let testTokens <- setRef.batchMintTestToken(playID: playID, quantity: %d) 745 signer.borrow<&BatchNFT.Collection>(from: /storage/TestTokenCollection)! 746 .batchDeposit(tokens: <-testTokens) 747 } 748 }` 749 mintScript := []byte(fmt.Sprintf(mintScriptTemplate, batchNFTAccount.Address.Hex(), size)) 750 751 txBody := flow.NewTransactionBody(). 752 SetGasLimit(999999). 753 SetScript(mintScript). 754 SetProposalKey(serviceAccount.Address, 0, serviceAccount.RetAndIncSeqNumber()). 755 AddAuthorizer(batchNFTAccount.Address). 756 SetPayer(serviceAccount.Address) 757 758 err := testutil.SignPayload(txBody, batchNFTAccount.Address, batchNFTAccount.PrivateKey) 759 require.NoError(b, err) 760 761 err = testutil.SignEnvelope(txBody, serviceAccount.Address, serviceAccount.PrivateKey) 762 require.NoError(b, err) 763 764 computationResult := be.ExecuteCollections(b, [][]*flow.TransactionBody{{txBody}}) 765 require.Empty(b, computationResult.TransactionResults[0].ErrorMessage) 766 } 767 768 func fundAccounts(b *testing.B, be TestBenchBlockExecutor, value cadence.UFix64, accounts ...flow.Address) { 769 serviceAccount := be.ServiceAccount(b) 770 for _, a := range accounts { 771 txBody := transferTokensTx(be.Chain(b)) 772 txBody.SetProposalKey(serviceAccount.Address, 0, serviceAccount.RetAndIncSeqNumber()) 773 txBody.AddArgument(jsoncdc.MustEncode(value)) 774 txBody.AddArgument(jsoncdc.MustEncode(cadence.Address(a))) 775 txBody.AddAuthorizer(serviceAccount.Address) 776 txBody.SetPayer(serviceAccount.Address) 777 778 err := testutil.SignEnvelope(txBody, serviceAccount.Address, serviceAccount.PrivateKey) 779 require.NoError(b, err) 780 781 computationResult := be.ExecuteCollections(b, [][]*flow.TransactionBody{{txBody}}) 782 require.Empty(b, computationResult.TransactionResults[0].ErrorMessage) 783 } 784 } 785 786 func deployBatchNFT(b *testing.B, be TestBenchBlockExecutor, owner *TestBenchAccount, nftAddress flow.Address) { 787 batchNFTContract := func(nftAddress flow.Address) string { 788 return fmt.Sprintf(` 789 import NonFungibleToken from 0x%s 790 791 pub contract BatchNFT: NonFungibleToken { 792 pub event ContractInitialized() 793 pub event PlayCreated(id: UInt32, metadata: {String:String}) 794 pub event NewSeriesStarted(newCurrentSeries: UInt32) 795 pub event SetCreated(setID: UInt32, series: UInt32) 796 pub event PlayAddedToSet(setID: UInt32, playID: UInt32) 797 pub event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numTestTokens: UInt32) 798 pub event SetLocked(setID: UInt32) 799 pub event TestTokenMinted(testTokenID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32) 800 pub event Withdraw(id: UInt64, from: Address?) 801 pub event Deposit(id: UInt64, to: Address?) 802 pub event TestTokenDestroyed(id: UInt64) 803 pub var currentSeries: UInt32 804 access(self) var playDatas: {UInt32: Play} 805 access(self) var setDatas: {UInt32: SetData} 806 access(self) var sets: @{UInt32: Set} 807 pub var nextPlayID: UInt32 808 pub var nextSetID: UInt32 809 pub var totalSupply: UInt64 810 811 pub struct Play { 812 pub let playID: UInt32 813 pub let metadata: {String: String} 814 815 init(metadata: {String: String}) { 816 pre { 817 metadata.length != 0: "New Play Metadata cannot be empty" 818 } 819 self.playID = BatchNFT.nextPlayID 820 self.metadata = metadata 821 822 BatchNFT.nextPlayID = BatchNFT.nextPlayID + UInt32(1) 823 emit PlayCreated(id: self.playID, metadata: metadata) 824 } 825 } 826 827 pub struct SetData { 828 pub let setID: UInt32 829 pub let name: String 830 pub let series: UInt32 831 init(name: String) { 832 pre { 833 name.length > 0: "New Set name cannot be empty" 834 } 835 self.setID = BatchNFT.nextSetID 836 self.name = name 837 self.series = BatchNFT.currentSeries 838 BatchNFT.nextSetID = BatchNFT.nextSetID + UInt32(1) 839 emit SetCreated(setID: self.setID, series: self.series) 840 } 841 } 842 843 pub resource Set { 844 pub let setID: UInt32 845 pub var plays: [UInt32] 846 pub var retired: {UInt32: Bool} 847 pub var locked: Bool 848 pub var numberMintedPerPlay: {UInt32: UInt32} 849 850 init(name: String) { 851 self.setID = BatchNFT.nextSetID 852 self.plays = [] 853 self.retired = {} 854 self.locked = false 855 self.numberMintedPerPlay = {} 856 857 BatchNFT.setDatas[self.setID] = SetData(name: name) 858 } 859 860 pub fun addPlay(playID: UInt32) { 861 pre { 862 BatchNFT.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist" 863 !self.locked: "Cannot add the play to the Set after the set has been locked" 864 self.numberMintedPerPlay[playID] == nil: "The play has already beed added to the set" 865 } 866 867 self.plays.append(playID) 868 self.retired[playID] = false 869 self.numberMintedPerPlay[playID] = 0 870 emit PlayAddedToSet(setID: self.setID, playID: playID) 871 } 872 873 pub fun addPlays(playIDs: [UInt32]) { 874 for play in playIDs { 875 self.addPlay(playID: play) 876 } 877 } 878 879 pub fun retirePlay(playID: UInt32) { 880 pre { 881 self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" 882 } 883 884 if !self.retired[playID]! { 885 self.retired[playID] = true 886 887 emit PlayRetiredFromSet(setID: self.setID, playID: playID, numTestTokens: self.numberMintedPerPlay[playID]!) 888 } 889 } 890 891 pub fun retireAll() { 892 for play in self.plays { 893 self.retirePlay(playID: play) 894 } 895 } 896 897 pub fun lock() { 898 if !self.locked { 899 self.locked = true 900 emit SetLocked(setID: self.setID) 901 } 902 } 903 904 pub fun mintTestToken(playID: UInt32): @NFT { 905 pre { 906 self.retired[playID] != nil: "Cannot mint the testToken: This play doesn't exist" 907 !self.retired[playID]!: "Cannot mint the testToken from this play: This play has been retired" 908 } 909 let numInPlay = self.numberMintedPerPlay[playID]! 910 let newTestToken: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1), 911 playID: playID, 912 setID: self.setID) 913 914 self.numberMintedPerPlay[playID] = numInPlay + UInt32(1) 915 916 return <-newTestToken 917 } 918 919 pub fun batchMintTestToken(playID: UInt32, quantity: UInt64): @Collection { 920 let newCollection <- create Collection() 921 922 var i: UInt64 = 0 923 while i < quantity { 924 newCollection.deposit(token: <-self.mintTestToken(playID: playID)) 925 i = i + UInt64(1) 926 } 927 928 return <-newCollection 929 } 930 } 931 932 pub struct TestTokenData { 933 pub let setID: UInt32 934 pub let playID: UInt32 935 pub let serialNumber: UInt32 936 937 init(setID: UInt32, playID: UInt32, serialNumber: UInt32) { 938 self.setID = setID 939 self.playID = playID 940 self.serialNumber = serialNumber 941 } 942 943 } 944 945 pub resource NFT: NonFungibleToken.INFT { 946 pub let id: UInt64 947 pub let data: TestTokenData 948 949 init(serialNumber: UInt32, playID: UInt32, setID: UInt32) { 950 BatchNFT.totalSupply = BatchNFT.totalSupply + UInt64(1) 951 952 self.id = BatchNFT.totalSupply 953 954 self.data = TestTokenData(setID: setID, playID: playID, serialNumber: serialNumber) 955 956 emit TestTokenMinted(testTokenID: self.id, playID: playID, setID: self.data.setID, serialNumber: self.data.serialNumber) 957 } 958 959 destroy() { 960 emit TestTokenDestroyed(id: self.id) 961 } 962 } 963 964 pub resource Admin { 965 pub fun createPlay(metadata: {String: String}): UInt32 { 966 var newPlay = Play(metadata: metadata) 967 let newID = newPlay.playID 968 969 BatchNFT.playDatas[newID] = newPlay 970 971 return newID 972 } 973 974 pub fun createSet(name: String) { 975 var newSet <- create Set(name: name) 976 977 BatchNFT.sets[newSet.setID] <-! newSet 978 } 979 980 pub fun borrowSet(setID: UInt32): &Set { 981 pre { 982 BatchNFT.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist" 983 } 984 return (&BatchNFT.sets[setID] as &Set?)! 985 } 986 987 pub fun startNewSeries(): UInt32 { 988 BatchNFT.currentSeries = BatchNFT.currentSeries + UInt32(1) 989 990 emit NewSeriesStarted(newCurrentSeries: BatchNFT.currentSeries) 991 992 return BatchNFT.currentSeries 993 } 994 995 pub fun createNewAdmin(): @Admin { 996 return <-create Admin() 997 } 998 } 999 1000 pub resource interface TestTokenCollectionPublic { 1001 pub fun deposit(token: @NonFungibleToken.NFT) 1002 pub fun batchDeposit(tokens: @NonFungibleToken.Collection) 1003 pub fun getIDs(): [UInt64] 1004 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT 1005 pub fun borrowTestToken(id: UInt64): &BatchNFT.NFT? { 1006 post { 1007 (result == nil) || (result?.id == id): 1008 "Cannot borrow TestToken reference: The ID of the returned reference is incorrect" 1009 } 1010 } 1011 } 1012 1013 pub resource Collection: TestTokenCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic { 1014 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} 1015 1016 init() { 1017 self.ownedNFTs <- {} 1018 } 1019 1020 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { 1021 let token <- self.ownedNFTs.remove(key: withdrawID) 1022 ?? panic("Cannot withdraw: TestToken does not exist in the collection") 1023 1024 emit Withdraw(id: token.id, from: self.owner?.address) 1025 1026 return <-token 1027 } 1028 1029 pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection { 1030 var batchCollection <- create Collection() 1031 1032 for id in ids { 1033 batchCollection.deposit(token: <-self.withdraw(withdrawID: id)) 1034 } 1035 return <-batchCollection 1036 } 1037 1038 pub fun deposit(token: @NonFungibleToken.NFT) { 1039 let token <- token as! @BatchNFT.NFT 1040 1041 let id = token.id 1042 let oldToken <- self.ownedNFTs[id] <- token 1043 1044 if self.owner?.address != nil { 1045 emit Deposit(id: id, to: self.owner?.address) 1046 } 1047 1048 destroy oldToken 1049 } 1050 1051 pub fun batchDeposit(tokens: @NonFungibleToken.Collection) { 1052 let keys = tokens.getIDs() 1053 1054 for key in keys { 1055 self.deposit(token: <-tokens.withdraw(withdrawID: key)) 1056 } 1057 destroy tokens 1058 } 1059 1060 pub fun getIDs(): [UInt64] { 1061 return self.ownedNFTs.keys 1062 } 1063 1064 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { 1065 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! 1066 } 1067 1068 pub fun borrowTestToken(id: UInt64): &BatchNFT.NFT? { 1069 if self.ownedNFTs[id] != nil { 1070 let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! 1071 return ref as! &BatchNFT.NFT 1072 } else { 1073 return nil 1074 } 1075 } 1076 destroy() { 1077 destroy self.ownedNFTs 1078 } 1079 } 1080 1081 pub fun createEmptyCollection(): @NonFungibleToken.Collection { 1082 return <-create BatchNFT.Collection() 1083 } 1084 1085 pub fun getAllPlays(): [BatchNFT.Play] { 1086 return BatchNFT.playDatas.values 1087 } 1088 1089 pub fun getPlayMetaData(playID: UInt32): {String: String}? { 1090 return self.playDatas[playID]?.metadata 1091 } 1092 1093 pub fun getPlayMetaDataByField(playID: UInt32, field: String): String? { 1094 if let play = BatchNFT.playDatas[playID] { 1095 return play.metadata[field] 1096 } else { 1097 return nil 1098 } 1099 } 1100 1101 pub fun getSetName(setID: UInt32): String? { 1102 return BatchNFT.setDatas[setID]?.name 1103 } 1104 1105 pub fun getSetSeries(setID: UInt32): UInt32? { 1106 return BatchNFT.setDatas[setID]?.series 1107 } 1108 1109 pub fun getSetIDsByName(setName: String): [UInt32]? { 1110 var setIDs: [UInt32] = [] 1111 1112 for setData in BatchNFT.setDatas.values { 1113 if setName == setData.name { 1114 setIDs.append(setData.setID) 1115 } 1116 } 1117 1118 if setIDs.length == 0 { 1119 return nil 1120 } else { 1121 return setIDs 1122 } 1123 } 1124 1125 pub fun getPlaysInSet(setID: UInt32): [UInt32]? { 1126 return BatchNFT.sets[setID]?.plays 1127 } 1128 1129 pub fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? { 1130 if let setToRead <- BatchNFT.sets.remove(key: setID) { 1131 let retired = setToRead.retired[playID] 1132 BatchNFT.sets[setID] <-! setToRead 1133 return retired 1134 } else { 1135 return nil 1136 } 1137 } 1138 1139 pub fun isSetLocked(setID: UInt32): Bool? { 1140 return BatchNFT.sets[setID]?.locked 1141 } 1142 1143 pub fun getNumTestTokensInEdition(setID: UInt32, playID: UInt32): UInt32? { 1144 if let setToRead <- BatchNFT.sets.remove(key: setID) { 1145 let amount = setToRead.numberMintedPerPlay[playID] 1146 BatchNFT.sets[setID] <-! setToRead 1147 return amount 1148 } else { 1149 return nil 1150 } 1151 } 1152 1153 init() { 1154 self.currentSeries = 0 1155 self.playDatas = {} 1156 self.setDatas = {} 1157 self.sets <- {} 1158 self.nextPlayID = 1 1159 self.nextSetID = 1 1160 self.totalSupply = 0 1161 1162 self.account.save<@Collection>(<- create Collection(), to: /storage/TestTokenCollection) 1163 self.account.link<&{TestTokenCollectionPublic}>(/public/TestTokenCollection, target: /storage/TestTokenCollection) 1164 self.account.save<@Admin>(<- create Admin(), to: /storage/BatchNFTAdmin) 1165 emit ContractInitialized() 1166 } 1167 } 1168 `, nftAddress.Hex()) 1169 } 1170 owner.DeployContract(b, be, "BatchNFT", batchNFTContract(nftAddress)) 1171 } 1172 1173 func deployNFT(b *testing.B, be TestBenchBlockExecutor, owner *TestBenchAccount) { 1174 const nftContract = ` 1175 pub contract interface NonFungibleToken { 1176 pub var totalSupply: UInt64 1177 pub event ContractInitialized() 1178 pub event Withdraw(id: UInt64, from: Address?) 1179 pub event Deposit(id: UInt64, to: Address?) 1180 pub resource interface INFT { 1181 pub let id: UInt64 1182 } 1183 pub resource NFT: INFT { 1184 pub let id: UInt64 1185 } 1186 pub resource interface Provider { 1187 pub fun withdraw(withdrawID: UInt64): @NFT { 1188 post { 1189 result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" 1190 } 1191 } 1192 } 1193 pub resource interface Receiver { 1194 pub fun deposit(token: @NFT) 1195 } 1196 pub resource interface CollectionPublic { 1197 pub fun deposit(token: @NFT) 1198 pub fun getIDs(): [UInt64] 1199 pub fun borrowNFT(id: UInt64): &NFT 1200 } 1201 pub resource Collection: Provider, Receiver, CollectionPublic { 1202 pub var ownedNFTs: @{UInt64: NFT} 1203 pub fun withdraw(withdrawID: UInt64): @NFT 1204 pub fun deposit(token: @NFT) 1205 pub fun getIDs(): [UInt64] 1206 pub fun borrowNFT(id: UInt64): &NFT { 1207 pre { 1208 self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" 1209 } 1210 } 1211 } 1212 pub fun createEmptyCollection(): @Collection { 1213 post { 1214 result.getIDs().length == 0: "The created collection must be empty!" 1215 } 1216 } 1217 }` 1218 1219 owner.DeployContract(b, be, "NonFungibleToken", nftContract) 1220 }