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 }