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