From 4eae55143f8a86671ec0a262807d88579548623c Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 20 Jan 2020 15:31:12 +0300 Subject: [PATCH] core: restrict GAS available for invocation transactions There are 10 GAS available for free plus any amount of GAS attached to a transaction. --- pkg/core/blockchain.go | 7 ++- pkg/core/gas_price.go | 112 +++++++++++++++++++++++++++++++++++ pkg/core/gas_price_test.go | 118 +++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 pkg/core/gas_price.go create mode 100644 pkg/core/gas_price_test.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index c24de0535..e95583ba0 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) + + gasAmount := util.Fixed8FromInt64(10) + t.Gas + v.SetGasLimit(gasAmount) + 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, } 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)) +}