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  }