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

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