diff --git a/config/config.go b/config/config.go index bda9d07c4..7a0ee3f52 100644 --- a/config/config.go +++ b/config/config.go @@ -54,6 +54,8 @@ type ( VerifyBlocks bool `yaml:"VerifyBlocks"` // Whether to verify transactions in received blocks. VerifyTransactions bool `yaml:"VerifyTransactions"` + // FreeGasLimit is an amount of GAS which can be spent for free. + FreeGasLimit util.Fixed8 `yaml:"FreeGasLimit"` } // SystemFee fees related to system. @@ -96,6 +98,9 @@ type ( EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"` Address string `yaml:"Address"` Port uint16 `yaml:"Port"` + // MaxGasInvoke is a maximum amount of gas which + // can be spent during RPC call. + MaxGasInvoke util.Fixed8 `yaml:"MaxGasInvoke"` } // NetMode describes the mode the blockchain will operate on. diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index 2e50755ae..5ce8872bb 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -29,6 +29,7 @@ ProtocolConfiguration: RegisterTransaction: 10000 VerifyBlocks: true VerifyTransactions: false + FreeGasLimit: 10.0 ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index f936dc261..6d5c2a910 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -29,6 +29,7 @@ ProtocolConfiguration: RegisterTransaction: 100 VerifyBlocks: true VerifyTransactions: false + FreeGasLimit: 10.0 ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index c24de0535..aca2e6b6f 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -514,6 +514,11 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { v := bc.spawnVMWithInterops(systemInterop) v.SetCheckedHash(tx.VerificationHash().BytesBE()) v.LoadScript(t.Script) + v.SetPriceGetter(getPrice) + if bc.config.FreeGasLimit >= 0 { + v.SetGasLimit(bc.config.FreeGasLimit + t.Gas) + } + err := v.Run() if !v.HasFailed() { _, err := systemInterop.dao.Persist() @@ -554,7 +559,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { TxHash: tx.Hash(), Trigger: trigger.Application, VMState: v.State(), - GasConsumed: util.Fixed8(0), + GasConsumed: v.GasConsumed(), Stack: v.Stack("estack"), Events: systemInterop.notifications, } @@ -1377,6 +1382,7 @@ func (bc *Blockchain) GetTestVM() (*vm.VM, storage.Store) { tmpStore := storage.NewMemCachedStore(bc.dao.store) systemInterop := bc.newInteropContext(trigger.Application, tmpStore, nil, nil) vm := bc.spawnVMWithInterops(systemInterop) + vm.SetPriceGetter(getPrice) return vm, tmpStore } diff --git a/pkg/core/gas_price.go b/pkg/core/gas_price.go new file mode 100644 index 000000000..e9ebdeaf0 --- /dev/null +++ b/pkg/core/gas_price.go @@ -0,0 +1,112 @@ +package core + +import ( + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/CityOfZion/neo-go/pkg/vm/opcode" +) + +// interopGasRatio is a multiplier by which a number returned from price getter +// and Fixed8 amount of GAS differ. Numbers defined in syscall tables are a multiple +// of 0.001 GAS = Fixed8(10^5). +const interopGasRatio = 100000 + +// getPrice returns a price for executing op with the provided parameter. +// Some SYSCALLs have variable price depending on their arguments. +func getPrice(v *vm.VM, op opcode.Opcode, parameter []byte) util.Fixed8 { + if op <= opcode.NOP { + return 0 + } + + switch op { + case opcode.APPCALL, opcode.TAILCALL: + return util.Fixed8FromInt64(10) + case opcode.SYSCALL: + interopID := vm.GetInteropID(parameter) + return getSyscallPrice(v, interopID) + case opcode.SHA1, opcode.SHA256: + return util.Fixed8FromInt64(10) + case opcode.HASH160, opcode.HASH256: + return util.Fixed8FromInt64(20) + case opcode.CHECKSIG, opcode.VERIFY: + return util.Fixed8FromInt64(100) + case opcode.CHECKMULTISIG: + estack := v.Estack() + if estack.Len() == 0 { + return util.Fixed8FromInt64(1) + } + + var cost int + + item := estack.Peek(0) + switch item.Item().(type) { + case *vm.ArrayItem, *vm.StructItem: + cost = len(item.Array()) + default: + cost = int(item.BigInt().Int64()) + } + + if cost < 1 { + return util.Fixed8FromInt64(1) + } + + return util.Fixed8FromInt64(int64(100 * cost)) + default: + return util.Fixed8FromInt64(1) + } +} + +// getSyscallPrice returns cost of executing syscall with provided id. +// Is SYSCALL is not found, cost is 1. +func getSyscallPrice(v *vm.VM, id uint32) util.Fixed8 { + ifunc := v.GetInteropByID(id) + if ifunc != nil && ifunc.Price > 0 { + return util.Fixed8(ifunc.Price) * interopGasRatio + } + + const ( + neoAssetCreate = 0x1fc6c583 // Neo.Asset.Create + antSharesAssetCreate = 0x99025068 // AntShares.Asset.Create + neoAssetRenew = 0x71908478 // Neo.Asset.Renew + antSharesAssetRenew = 0xaf22447b // AntShares.Asset.Renew + neoContractCreate = 0x6ea56cf6 // Neo.Contract.Create + neoContractMigrate = 0x90621b47 // Neo.Contract.Migrate + antSharesContractCreate = 0x2a28d29b // AntShares.Contract.Create + antSharesContractMigrate = 0xa934c8bb // AntShares.Contract.Migrate + systemStoragePut = 0x84183fe6 // System.Storage.Put + systemStoragePutEx = 0x3a9be173 // System.Storage.PutEx + neoStoragePut = 0xf541a152 // Neo.Storage.Put + antSharesStoragePut = 0x5f300a9e // AntShares.Storage.Put + ) + + estack := v.Estack() + + switch id { + case neoAssetCreate, antSharesAssetCreate: + return util.Fixed8FromInt64(5000) + case neoAssetRenew, antSharesAssetRenew: + arg := estack.Peek(1).BigInt().Int64() + return util.Fixed8FromInt64(arg * 5000) + case neoContractCreate, neoContractMigrate, antSharesContractCreate, antSharesContractMigrate: + fee := int64(100) + props := smartcontract.PropertyState(estack.Peek(3).BigInt().Int64()) + + if props&smartcontract.HasStorage != 0 { + fee += 400 + } + + if props&smartcontract.HasDynamicInvoke != 0 { + fee += 500 + } + + return util.Fixed8FromInt64(fee) + case systemStoragePut, systemStoragePutEx, neoStoragePut, antSharesStoragePut: + // price for storage PUT is 1 GAS per 1 KiB + keySize := len(estack.Peek(1).Bytes()) + valSize := len(estack.Peek(2).Bytes()) + return util.Fixed8FromInt64(int64((keySize+valSize-1)/1024 + 1)) + default: + return util.Fixed8FromInt64(1) + } +} diff --git a/pkg/core/gas_price_test.go b/pkg/core/gas_price_test.go new file mode 100644 index 000000000..e2c9fb761 --- /dev/null +++ b/pkg/core/gas_price_test.go @@ -0,0 +1,118 @@ +package core + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/smartcontract/trigger" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/stretchr/testify/require" +) + +// These tests are taken from C# code +// https://github.com/neo-project/neo/blob/master-2.x/neo.UnitTests/UT_InteropPrices.cs#L245 +func TestGetPrice(t *testing.T) { + bc := newTestChain(t) + systemInterop := bc.newInteropContext(trigger.Application, storage.NewMemoryStore(), nil, nil) + + v := bc.spawnVMWithInterops(systemInterop) + v.SetPriceGetter(getPrice) + + t.Run("Neo.Asset.Create", func(t *testing.T) { + // Neo.Asset.Create: 83c5c61f + v.Load([]byte{0x68, 0x04, 0x83, 0xc5, 0xc6, 0x1f}) + checkGas(t, util.Fixed8FromInt64(5000), v) + }) + + t.Run("Neo.Asset.Renew", func(t *testing.T) { + // Neo.Asset.Renew: 78849071 (requires push 09 push 09 before) + v.Load([]byte{0x59, 0x59, 0x68, 0x04, 0x78, 0x84, 0x90, 0x71}) + require.NoError(t, v.StepInto()) // push 9 + require.NoError(t, v.StepInto()) // push 9 + + checkGas(t, util.Fixed8FromInt64(9*5000), v) + }) + + t.Run("Neo.Contract.Create (no props)", func(t *testing.T) { + // Neo.Contract.Create: f66ca56e (requires push properties on fourth position) + v.Load([]byte{0x00, 0x00, 0x00, 0x00, 0x68, 0x04, 0xf6, 0x6c, 0xa5, 0x6e}) + require.NoError(t, v.StepInto()) // push 0 - ContractPropertyState.NoProperty + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + + checkGas(t, util.Fixed8FromInt64(100), v) + }) + + t.Run("Neo.Contract.Create (has storage)", func(t *testing.T) { + // Neo.Contract.Create: f66ca56e (requires push properties on fourth position) + v.Load([]byte{0x51, 0x00, 0x00, 0x00, 0x68, 0x04, 0xf6, 0x6c, 0xa5, 0x6e}) + require.NoError(t, v.StepInto()) // push 01 - ContractPropertyState.HasStorage + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + + checkGas(t, util.Fixed8FromInt64(500), v) + }) + + t.Run("Neo.Contract.Create (has dynamic invoke)", func(t *testing.T) { + // Neo.Contract.Create: f66ca56e (requires push properties on fourth position) + v.Load([]byte{0x52, 0x00, 0x00, 0x00, 0x68, 0x04, 0xf6, 0x6c, 0xa5, 0x6e}) + require.NoError(t, v.StepInto()) // push 02 - ContractPropertyState.HasDynamicInvoke + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + + checkGas(t, util.Fixed8FromInt64(600), v) + }) + + t.Run("Neo.Contract.Create (has both storage and dynamic invoke)", func(t *testing.T) { + // Neo.Contract.Create: f66ca56e (requires push properties on fourth position) + v.Load([]byte{0x53, 0x00, 0x00, 0x00, 0x68, 0x04, 0xf6, 0x6c, 0xa5, 0x6e}) + require.NoError(t, v.StepInto()) // push 03 - HasStorage and HasDynamicInvoke + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + + checkGas(t, util.Fixed8FromInt64(1000), v) + }) + + t.Run("Neo.Contract.Migrate", func(t *testing.T) { + // Neo.Contract.Migrate: 471b6290 (requires push properties on fourth position) + v.Load([]byte{0x00, 0x00, 0x00, 0x00, 0x68, 0x04, 0x47, 0x1b, 0x62, 0x90}) + require.NoError(t, v.StepInto()) // push 0 - ContractPropertyState.NoProperty + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + require.NoError(t, v.StepInto()) // push 0 + + checkGas(t, util.Fixed8FromInt64(100), v) + }) + + t.Run("System.Storage.Put", func(t *testing.T) { + // System.Storage.Put: e63f1884 (requires push key and value) + v.Load([]byte{0x53, 0x53, 0x00, 0x68, 0x04, 0xe6, 0x3f, 0x18, 0x84}) + require.NoError(t, v.StepInto()) // push 03 (length 1) + require.NoError(t, v.StepInto()) // push 03 (length 1) + require.NoError(t, v.StepInto()) // push 00 + + checkGas(t, util.Fixed8FromInt64(1), v) + }) + + t.Run("System.Storage.PutEx", func(t *testing.T) { + // System.Storage.PutEx: 73e19b3a (requires push key and value) + v.Load([]byte{0x53, 0x53, 0x00, 0x68, 0x04, 0x73, 0xe1, 0x9b, 0x3a}) + require.NoError(t, v.StepInto()) // push 03 (length 1) + require.NoError(t, v.StepInto()) // push 03 (length 1) + require.NoError(t, v.StepInto()) // push 00 + + checkGas(t, util.Fixed8FromInt64(1), v) + }) +} + +func checkGas(t *testing.T, expected util.Fixed8, v *vm.VM) { + op, par, err := v.Context().Next() + + require.NoError(t, err) + require.Equal(t, expected, getPrice(v, op, par)) +} diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 3bc5cd451..b2cd26f56 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -396,11 +396,12 @@ func (s *Server) invokescript(reqParams Params) (interface{}, error) { // result. func (s *Server) runScriptInVM(script []byte) *wrappers.InvokeResult { vm, _ := s.chain.GetTestVM() + vm.SetGasLimit(s.config.MaxGasInvoke) vm.LoadScript(script) _ = vm.Run() result := &wrappers.InvokeResult{ State: vm.State(), - GasConsumed: "0.1", + GasConsumed: vm.GasConsumed().String(), Script: hex.EncodeToString(script), Stack: vm.Estack(), } diff --git a/pkg/util/fixed8.go b/pkg/util/fixed8.go index 617d0f48d..1282da8b8 100644 --- a/pkg/util/fixed8.go +++ b/pkg/util/fixed8.go @@ -87,8 +87,20 @@ func Fixed8FromString(s string) (Fixed8, error) { // UnmarshalJSON implements the json unmarshaller interface. func (f *Fixed8) UnmarshalJSON(data []byte) error { + return f.unmarshalHelper(func(v interface{}) error { + return json.Unmarshal(data, v) + }) +} + +// UnmarshalYAML implements the yaml unmarshaler interface. +func (f *Fixed8) UnmarshalYAML(unmarshal func(interface{}) error) error { + return f.unmarshalHelper(unmarshal) +} + +// unmarshalHelper is an underlying unmarshaller func for JSON and YAML. +func (f *Fixed8) unmarshalHelper(unmarshal func(interface{}) error) error { var s string - if err := json.Unmarshal(data, &s); err == nil { + if err := unmarshal(&s); err == nil { p, err := Fixed8FromString(s) if err != nil { return err @@ -98,7 +110,7 @@ func (f *Fixed8) UnmarshalJSON(data []byte) error { } var fl float64 - if err := json.Unmarshal(data, &fl); err != nil { + if err := unmarshal(&fl); err != nil { return err } @@ -111,6 +123,11 @@ func (f Fixed8) MarshalJSON() ([]byte, error) { return []byte(`"` + f.String() + `"`), nil } +// MarshalYAML implements the yaml marshaller interface. +func (f Fixed8) MarshalYAML() (interface{}, error) { + return f.String(), nil +} + // DecodeBinary implements the io.Serializable interface. func (f *Fixed8) DecodeBinary(r *io.BinReader) { *f = Fixed8(r.ReadU64LE()) diff --git a/pkg/util/fixed8_test.go b/pkg/util/fixed8_test.go index 30753a7f2..7b2c76463 100644 --- a/pkg/util/fixed8_test.go +++ b/pkg/util/fixed8_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/CityOfZion/neo-go/pkg/io" + "github.com/go-yaml/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -125,6 +126,19 @@ func TestFixed8_MarshalJSON(t *testing.T) { assert.Equal(t, []byte(`"123.4"`), s) } +func TestFixed8_UnmarshalYAML(t *testing.T) { + u, err := Fixed8FromString("123.4") + assert.NoError(t, err) + + s, err := yaml.Marshal(u) + assert.NoError(t, err) + assert.Equal(t, []byte("\"123.4\"\n"), s) // yaml marshaler inserts LF at the end + + var f Fixed8 + assert.NoError(t, yaml.Unmarshal([]byte(`"123.4"`), &f)) + assert.Equal(t, u, f) +} + func TestFixed8_Arith(t *testing.T) { u1 := Fixed8FromInt64(3) u2 := Fixed8FromInt64(8) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index eae4aaab5..086ab630b 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -63,6 +63,9 @@ type VM struct { // callbacks to get interops. getInterop []InteropGetterFunc + // callback to get interop price + getPrice func(*VM, opcode.Opcode, []byte) util.Fixed8 + // callback to get scripts. getScript func(util.Uint160) []byte @@ -76,6 +79,9 @@ type VM struct { itemCount map[StackItem]int size int + gasConsumed util.Fixed8 + gasLimit util.Fixed8 + // Public keys cache. keys map[string]*keys.PublicKey } @@ -114,6 +120,23 @@ func (v *VM) RegisterInteropGetter(f InteropGetterFunc) { v.getInterop = append(v.getInterop, f) } +// SetPriceGetter registers the given PriceGetterFunc in v. +// f accepts vm's Context, current instruction and instruction parameter. +func (v *VM) SetPriceGetter(f func(*VM, opcode.Opcode, []byte) util.Fixed8) { + v.getPrice = f +} + +// GasConsumed returns the amount of GAS consumed during execution. +func (v *VM) GasConsumed() util.Fixed8 { + return v.gasConsumed +} + +// SetGasLimit sets maximum amount of gas which v can spent. +// If max <= 0, no limit is imposed. +func (v *VM) SetGasLimit(max util.Fixed8) { + v.gasLimit = max +} + // Estack returns the evaluation stack so interop hooks can utilize this. func (v *VM) Estack() *Stack { return v.estack @@ -225,6 +248,7 @@ func (v *VM) Load(prog []byte) { v.estack.Clear() v.astack.Clear() v.state = noneState + v.gasConsumed = 0 v.LoadScript(prog) } @@ -450,6 +474,27 @@ func (v *VM) SetScriptGetter(gs func(util.Uint160) []byte) { v.getScript = gs } +// GetInteropID converts instruction parameter to an interop ID. +func GetInteropID(parameter []byte) uint32 { + if len(parameter) == 4 { + return binary.LittleEndian.Uint32(parameter) + } + + return InteropNameToID(parameter) +} + +// GetInteropByID returns interop function together with price. +// Registered callbacks are checked in LIFO order. +func (v *VM) GetInteropByID(id uint32) *InteropFuncPrice { + for i := len(v.getInterop) - 1; i >= 0; i-- { + if ifunc := v.getInterop[i](id); ifunc != nil { + return ifunc + } + } + + return nil +} + // execute performs an instruction cycle in the VM. Acting on the instruction (opcode). func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err error) { // Instead of polluting the whole VM logic with error handling, we will recover @@ -464,6 +509,13 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro } }() + if v.getPrice != nil && ctx.ip < len(ctx.prog) { + v.gasConsumed += v.getPrice(v, op, parameter) + if v.gasLimit > 0 && v.gasConsumed > v.gasLimit { + panic("gas limit is exceeded") + } + } + if op >= opcode.PUSHBYTES1 && op <= opcode.PUSHBYTES75 { v.estack.PushVal(parameter) return @@ -1053,51 +1105,28 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro v.estack.PushVal(len(arr)) case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT: - var ( - rOffset = int16(binary.LittleEndian.Uint16(parameter)) - offset = ctx.ip + int(rOffset) - ) - if offset < 0 || offset > len(ctx.prog) { - panic(fmt.Sprintf("JMP: invalid offset %d ip at %d", offset, ctx.ip)) - } + offset := v.getJumpOffset(ctx, parameter) cond := true - if op > opcode.JMP { - cond = v.estack.Pop().Bool() - if op == opcode.JMPIFNOT { - cond = !cond - } - } - if cond { - ctx.nextip = offset + if op != opcode.JMP { + cond = v.estack.Pop().Bool() == (op == opcode.JMPIF) } + v.jumpIf(ctx, offset, cond) + case opcode.CALL: v.checkInvocationStackSize() newCtx := ctx.Copy() newCtx.rvcount = -1 v.istack.PushVal(newCtx) - err = v.execute(v.Context(), opcode.JMP, parameter) - if err != nil { - return - } + + offset := v.getJumpOffset(newCtx, parameter) + v.jumpIf(newCtx, offset, true) case opcode.SYSCALL: - var ifunc *InteropFuncPrice - var interopID uint32 + interopID := GetInteropID(parameter) + ifunc := v.GetInteropByID(interopID) - if len(parameter) == 4 { - interopID = binary.LittleEndian.Uint32(parameter) - } else { - interopID = InteropNameToID(parameter) - } - // LIFO interpretation of callbacks. - for i := len(v.getInterop) - 1; i >= 0; i-- { - ifunc = v.getInterop[i](interopID) - if ifunc != nil { - break - } - } if ifunc == nil { panic(fmt.Sprintf("interop hook (%q/0x%x) not registered", parameter, interopID)) } @@ -1361,10 +1390,8 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro v.estack = newCtx.estack v.astack = newCtx.astack if op == opcode.CALLI { - err = v.execute(v.Context(), opcode.JMP, parameter[2:]) - if err != nil { - return - } + offset := v.getJumpOffset(newCtx, parameter[2:]) + v.jumpIf(newCtx, offset, true) } case opcode.THROW: @@ -1381,6 +1408,26 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro return } +// jumpIf performs jump to offset if cond is true. +func (v *VM) jumpIf(ctx *Context, offset int, cond bool) { + if cond { + ctx.nextip = offset + } +} + +// getJumpOffset returns instruction number in a current context +// to a which JMP should be performed. +// parameter is interpreted as little-endian int16. +func (v *VM) getJumpOffset(ctx *Context, parameter []byte) int { + rOffset := int16(binary.LittleEndian.Uint16(parameter)) + offset := ctx.ip + int(rOffset) + if offset < 0 || offset > len(ctx.prog) { + panic(fmt.Sprintf("JMP: invalid offset %d ip at %d", offset, ctx.ip)) + } + + return offset +} + func cloneIfStruct(item StackItem) StackItem { switch it := item.(type) { case *StructItem: diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 2e9fb9ae2..cab01864a 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -62,6 +62,54 @@ func TestRegisterInteropGetter(t *testing.T) { assert.Equal(t, currRegistered+1, len(v.getInterop)) } +func TestVM_SetPriceGetter(t *testing.T) { + v := New() + prog := []byte{ + byte(opcode.PUSH4), byte(opcode.PUSH2), + byte(opcode.PUSHDATA1), 0x01, 0x01, + byte(opcode.PUSHDATA1), 0x02, 0xCA, 0xFE, + byte(opcode.PUSH4), byte(opcode.RET), + } + + t.Run("no price getter", func(t *testing.T) { + v.Load(prog) + runVM(t, v) + + require.EqualValues(t, 0, v.GasConsumed()) + }) + + v.SetPriceGetter(func(_ *VM, op opcode.Opcode, p []byte) util.Fixed8 { + if op == opcode.PUSH4 { + return 1 + } else if op == opcode.PUSHDATA1 && bytes.Equal(p, []byte{0xCA, 0xFE}) { + return 7 + } + + return 0 + }) + + t.Run("with price getter", func(t *testing.T) { + v.Load(prog) + runVM(t, v) + + require.EqualValues(t, 9, v.GasConsumed()) + }) + + t.Run("with sufficient gas limit", func(t *testing.T) { + v.Load(prog) + v.SetGasLimit(9) + runVM(t, v) + + require.EqualValues(t, 9, v.GasConsumed()) + }) + + t.Run("with small gas limit", func(t *testing.T) { + v.Load(prog) + v.SetGasLimit(8) + checkVMFailed(t, v) + }) +} + func TestBytesToPublicKey(t *testing.T) { v := New() cache := v.GetPublicKeys()