github.com/onflow/flow-go@v0.33.17/fvm/fvm_bench_test.go (about)

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