github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/fvm/fvm_fuzz_test.go (about)

     1  package fvm_test
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"testing"
     7  
     8  	"github.com/onflow/cadence/runtime/stdlib"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/onflow/cadence"
    12  	"github.com/onflow/cadence/encoding/ccf"
    13  	jsoncdc "github.com/onflow/cadence/encoding/json"
    14  
    15  	"github.com/onflow/flow-go/engine/execution/testutil"
    16  	"github.com/onflow/flow-go/fvm"
    17  	"github.com/onflow/flow-go/fvm/environment"
    18  	"github.com/onflow/flow-go/fvm/errors"
    19  	"github.com/onflow/flow-go/fvm/meter"
    20  	"github.com/onflow/flow-go/fvm/storage/snapshot"
    21  	"github.com/onflow/flow-go/fvm/systemcontracts"
    22  	"github.com/onflow/flow-go/model/flow"
    23  	"github.com/onflow/flow-go/utils/unittest"
    24  )
    25  
    26  func FuzzTransactionComputationLimit(f *testing.F) {
    27  	// setup initial state
    28  	vmt, tctx := bootstrapFuzzStateAndTxContext(f)
    29  
    30  	f.Add(uint64(0), uint64(0), uint64(0), uint(0))
    31  	f.Add(uint64(5), uint64(0), uint64(0), uint(0))
    32  	f.Fuzz(func(t *testing.T, computationLimit uint64, memoryLimit uint64, interactionLimit uint64, transactionType uint) {
    33  		computationLimit %= flow.DefaultMaxTransactionGasLimit
    34  		transactionType %= uint(len(fuzzTransactionTypes))
    35  
    36  		tt := fuzzTransactionTypes[transactionType]
    37  
    38  		vmt.run(func(t *testing.T, vm fvm.VM, chain flow.Chain, ctx fvm.Context, snapshotTree snapshot.SnapshotTree) {
    39  			// create the transaction
    40  			txBody := tt.createTxBody(t, tctx)
    41  			// set the computation limit
    42  			txBody.SetComputeLimit(computationLimit)
    43  
    44  			// sign the transaction
    45  			err := testutil.SignEnvelope(
    46  				txBody,
    47  				tctx.address,
    48  				tctx.privateKey,
    49  			)
    50  			require.NoError(t, err)
    51  
    52  			// set the memory limit
    53  			ctx.MemoryLimit = memoryLimit
    54  			// set the interaction limit
    55  			ctx.MaxStateInteractionSize = interactionLimit
    56  
    57  			var output fvm.ProcedureOutput
    58  
    59  			// run the transaction
    60  			require.NotPanics(t, func() {
    61  				_, output, err = vm.Run(
    62  					ctx,
    63  					fvm.Transaction(txBody, 0),
    64  					snapshotTree)
    65  			}, "Transaction should never result in a panic.")
    66  			require.NoError(t, err, "Transaction should never result in an error.")
    67  
    68  			// check if results are expected
    69  			tt.require(t, tctx, fuzzResults{
    70  				output: output,
    71  			})
    72  		})(t)
    73  	})
    74  }
    75  
    76  type fuzzResults struct {
    77  	output fvm.ProcedureOutput
    78  }
    79  
    80  type transactionTypeContext struct {
    81  	address      flow.Address
    82  	addressFunds uint64
    83  	privateKey   flow.AccountPrivateKey
    84  	chain        flow.Chain
    85  }
    86  
    87  type transactionType struct {
    88  	createTxBody func(t *testing.T, tctx transactionTypeContext) *flow.TransactionBody
    89  	require      func(t *testing.T, tctx transactionTypeContext, results fuzzResults)
    90  }
    91  
    92  var fuzzTransactionTypes = []transactionType{
    93  	{
    94  		// Token transfer of 0 tokens.
    95  		// should succeed if no limits are hit.
    96  		// fees should be deducted no matter what.
    97  		createTxBody: func(t *testing.T, tctx transactionTypeContext) *flow.TransactionBody {
    98  			txBody := transferTokensTx(tctx.chain).
    99  				AddAuthorizer(tctx.address).
   100  				AddArgument(jsoncdc.MustEncode(cadence.UFix64(0))). // 0 value transferred
   101  				AddArgument(jsoncdc.MustEncode(cadence.NewAddress(tctx.chain.ServiceAddress())))
   102  
   103  			txBody.SetProposalKey(tctx.address, 0, 0)
   104  			txBody.SetPayer(tctx.address)
   105  			return txBody
   106  		},
   107  		require: func(t *testing.T, tctx transactionTypeContext, results fuzzResults) {
   108  			// if there is an error, it should be computation exceeded
   109  			if results.output.Err != nil {
   110  				require.Len(t, results.output.Events, 5)
   111  				unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   112  				codes := []errors.ErrorCode{
   113  					errors.ErrCodeComputationLimitExceededError,
   114  					errors.ErrCodeCadenceRunTimeError,
   115  					errors.ErrCodeLedgerInteractionLimitExceededError,
   116  				}
   117  				require.Contains(t, codes, results.output.Err.Code(), results.output.Err.Error())
   118  			}
   119  
   120  			// fees should be deducted no matter the input
   121  			fees, deducted := getDeductedFees(t, tctx, results)
   122  			require.True(t, deducted, "Fees should be deducted.")
   123  			require.GreaterOrEqual(
   124  				t,
   125  				uint64(fees),
   126  				fuzzTestsInclusionFees,
   127  			)
   128  			unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   129  		},
   130  	},
   131  	{
   132  		// Token transfer of too many tokens.
   133  		// Should never succeed.
   134  		// fees should be deducted no matter what.
   135  		createTxBody: func(t *testing.T, tctx transactionTypeContext) *flow.TransactionBody {
   136  			txBody := transferTokensTx(tctx.chain).
   137  				AddAuthorizer(tctx.address).
   138  				AddArgument(jsoncdc.MustEncode(cadence.UFix64(2 * tctx.addressFunds))). // too much value transferred
   139  				AddArgument(jsoncdc.MustEncode(cadence.NewAddress(tctx.chain.ServiceAddress())))
   140  
   141  			txBody.SetProposalKey(tctx.address, 0, 0)
   142  			txBody.SetPayer(tctx.address)
   143  			return txBody
   144  		},
   145  		require: func(t *testing.T, tctx transactionTypeContext, results fuzzResults) {
   146  			require.Error(t, results.output.Err)
   147  			require.Len(t, results.output.Events, 3)
   148  			unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   149  			codes := []errors.ErrorCode{
   150  				errors.ErrCodeComputationLimitExceededError,
   151  				errors.ErrCodeCadenceRunTimeError, // because of the failed transfer
   152  				errors.ErrCodeLedgerInteractionLimitExceededError,
   153  			}
   154  			require.Contains(t, codes, results.output.Err.Code(), results.output.Err.Error())
   155  
   156  			// fees should be deducted no matter the input
   157  			fees, deducted := getDeductedFees(t, tctx, results)
   158  			require.True(t, deducted, "Fees should be deducted.")
   159  			require.GreaterOrEqual(t,
   160  				uint64(fees),
   161  				fuzzTestsInclusionFees,
   162  			)
   163  			unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   164  		},
   165  	},
   166  	{
   167  		// Transaction that calls panic.
   168  		// Should never succeed.
   169  		// fees should be deducted no matter what.
   170  		createTxBody: func(t *testing.T, tctx transactionTypeContext) *flow.TransactionBody {
   171  			// empty transaction
   172  			txBody := flow.NewTransactionBody().SetScript([]byte("transaction(){prepare(){};execute{panic(\"some panic\")}}"))
   173  			txBody.SetProposalKey(tctx.address, 0, 0)
   174  			txBody.SetPayer(tctx.address)
   175  			return txBody
   176  		},
   177  		require: func(t *testing.T, tctx transactionTypeContext, results fuzzResults) {
   178  			require.Error(t, results.output.Err)
   179  			require.Len(t, results.output.Events, 3)
   180  			unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   181  			codes := []errors.ErrorCode{
   182  				errors.ErrCodeComputationLimitExceededError,
   183  				errors.ErrCodeCadenceRunTimeError, // because of the panic
   184  				errors.ErrCodeLedgerInteractionLimitExceededError,
   185  			}
   186  			require.Contains(t, codes, results.output.Err.Code(), results.output.Err.Error())
   187  
   188  			// fees should be deducted no matter the input
   189  			fees, deducted := getDeductedFees(t, tctx, results)
   190  			require.True(t, deducted, "Fees should be deducted.")
   191  			require.GreaterOrEqual(t,
   192  				uint64(fees),
   193  				fuzzTestsInclusionFees,
   194  			)
   195  			unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   196  		},
   197  	},
   198  	{
   199  		createTxBody: func(t *testing.T, tctx transactionTypeContext) *flow.TransactionBody {
   200  			// create account
   201  			txBody := flow.NewTransactionBody().SetScript(createAccountScript).
   202  				AddAuthorizer(tctx.address)
   203  			txBody.SetProposalKey(tctx.address, 0, 0)
   204  			txBody.SetPayer(tctx.address)
   205  			return txBody
   206  		},
   207  		require: func(t *testing.T, tctx transactionTypeContext, results fuzzResults) {
   208  			// if there is an error, it should be computation exceeded
   209  			if results.output.Err != nil {
   210  				require.Len(t, results.output.Events, 3)
   211  				unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   212  				codes := []errors.ErrorCode{
   213  					errors.ErrCodeComputationLimitExceededError,
   214  					errors.ErrCodeCadenceRunTimeError,
   215  					errors.ErrCodeLedgerInteractionLimitExceededError,
   216  				}
   217  				require.Contains(t, codes, results.output.Err.Code(), results.output.Err.Error())
   218  			}
   219  
   220  			// fees should be deducted no matter the input
   221  			fees, deducted := getDeductedFees(t, tctx, results)
   222  			require.True(t, deducted, "Fees should be deducted.")
   223  			require.GreaterOrEqual(t,
   224  				uint64(fees),
   225  				fuzzTestsInclusionFees,
   226  			)
   227  			unittest.EnsureEventsIndexSeq(t, results.output.Events, tctx.chain.ChainID())
   228  		},
   229  	},
   230  }
   231  
   232  const fuzzTestsInclusionFees = uint64(1_000)
   233  
   234  // checks fee deduction happened and returns the amount of funds deducted
   235  func getDeductedFees(tb testing.TB, tctx transactionTypeContext, results fuzzResults) (fees cadence.UFix64, deducted bool) {
   236  	tb.Helper()
   237  
   238  	sc := systemcontracts.SystemContractsForChain(tctx.chain.ChainID())
   239  
   240  	var ok bool
   241  	var feesDeductedEvent cadence.Event
   242  	for _, e := range results.output.Events {
   243  		if string(e.Type) == fmt.Sprintf("A.%s.FlowFees.FeesDeducted", sc.FlowFees.Address.Hex()) {
   244  			data, err := ccf.Decode(nil, e.Payload)
   245  			require.NoError(tb, err)
   246  			feesDeductedEvent, ok = data.(cadence.Event)
   247  			require.True(tb, ok, "Event payload should be of type cadence event")
   248  		}
   249  	}
   250  	if feesDeductedEvent.Type() == nil {
   251  		return 0, false
   252  	}
   253  
   254  	feesValue := cadence.SearchFieldByName(feesDeductedEvent, "amount")
   255  	require.IsType(tb,
   256  		cadence.UFix64(0),
   257  		feesValue,
   258  		"FeesDeducted event amount field should be of type cadence.UFix64",
   259  	)
   260  
   261  	return feesValue.(cadence.UFix64), true
   262  }
   263  
   264  func bootstrapFuzzStateAndTxContext(tb testing.TB) (bootstrappedVmTest, transactionTypeContext) {
   265  	tb.Helper()
   266  
   267  	addressFunds := uint64(1_000_000_000)
   268  	var privateKey flow.AccountPrivateKey
   269  	var address flow.Address
   270  	bootstrappedVMTest, err := newVMTest().withBootstrapProcedureOptions(
   271  		fvm.WithTransactionFee(fvm.DefaultTransactionFees),
   272  		fvm.WithExecutionMemoryLimit(math.MaxUint32),
   273  		fvm.WithExecutionEffortWeights(environment.MainnetExecutionEffortWeights),
   274  		fvm.WithExecutionMemoryWeights(meter.DefaultMemoryWeights),
   275  		fvm.WithMinimumStorageReservation(fvm.DefaultMinimumStorageReservation),
   276  		fvm.WithAccountCreationFee(fvm.DefaultAccountCreationFee),
   277  		fvm.WithStorageMBPerFLOW(fvm.DefaultStorageMBPerFLOW),
   278  	).withContextOptions(
   279  		fvm.WithTransactionFeesEnabled(true),
   280  		fvm.WithAccountStorageLimit(true),
   281  	).bootstrapWith(func(vm fvm.VM, chain flow.Chain, ctx fvm.Context, snapshotTree snapshot.SnapshotTree) (snapshot.SnapshotTree, error) {
   282  		// ==== Create an account ====
   283  		var txBody *flow.TransactionBody
   284  		privateKey, txBody = testutil.CreateAccountCreationTransaction(tb, chain)
   285  
   286  		err := testutil.SignTransactionAsServiceAccount(txBody, 0, chain)
   287  		if err != nil {
   288  			return snapshotTree, err
   289  		}
   290  
   291  		executionSnapshot, output, err := vm.Run(
   292  			ctx,
   293  			fvm.Transaction(txBody, 0),
   294  			snapshotTree)
   295  		require.NoError(tb, err)
   296  		require.NoError(tb, output.Err)
   297  
   298  		snapshotTree = snapshotTree.Append(executionSnapshot)
   299  
   300  		accountCreatedEvents := filterAccountCreatedEvents(output.Events)
   301  
   302  		// read the address of the account created (e.g. "0x01" and convert it to flow.address)
   303  		data, err := ccf.Decode(nil, accountCreatedEvents[0].Payload)
   304  		require.NoError(tb, err)
   305  
   306  		address = flow.ConvertAddress(
   307  			cadence.SearchFieldByName(
   308  				data.(cadence.Event),
   309  				stdlib.AccountEventAddressParameter.Identifier,
   310  			).(cadence.Address),
   311  		)
   312  
   313  		// ==== Transfer tokens to new account ====
   314  		txBody = transferTokensTx(chain).
   315  			AddAuthorizer(chain.ServiceAddress()).
   316  			AddArgument(jsoncdc.MustEncode(cadence.UFix64(1_000_000_000))). // 10 FLOW
   317  			AddArgument(jsoncdc.MustEncode(cadence.NewAddress(address)))
   318  
   319  		txBody.SetProposalKey(chain.ServiceAddress(), 0, 1)
   320  		txBody.SetPayer(chain.ServiceAddress())
   321  
   322  		err = testutil.SignEnvelope(
   323  			txBody,
   324  			chain.ServiceAddress(),
   325  			unittest.ServiceAccountPrivateKey,
   326  		)
   327  		require.NoError(tb, err)
   328  
   329  		executionSnapshot, output, err = vm.Run(
   330  			ctx,
   331  			fvm.Transaction(txBody, 0),
   332  			snapshotTree)
   333  		if err != nil {
   334  			return snapshotTree, err
   335  		}
   336  
   337  		return snapshotTree.Append(executionSnapshot), output.Err
   338  	})
   339  	require.NoError(tb, err)
   340  
   341  	return bootstrappedVMTest,
   342  		transactionTypeContext{
   343  			address:      address,
   344  			addressFunds: addressFunds,
   345  			privateKey:   privateKey,
   346  			chain:        bootstrappedVMTest.chain,
   347  		}
   348  }