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 }