forked from TrueCloudLab/neoneo-go
Merge pull request #609 from nspcc-dev/feature/gas
vm: calculate GAS spent, fix #607.
This commit is contained in:
commit
01e16e68ad
11 changed files with 411 additions and 41 deletions
|
@ -54,6 +54,8 @@ type (
|
||||||
VerifyBlocks bool `yaml:"VerifyBlocks"`
|
VerifyBlocks bool `yaml:"VerifyBlocks"`
|
||||||
// Whether to verify transactions in received blocks.
|
// Whether to verify transactions in received blocks.
|
||||||
VerifyTransactions bool `yaml:"VerifyTransactions"`
|
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.
|
// SystemFee fees related to system.
|
||||||
|
@ -96,6 +98,9 @@ type (
|
||||||
EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"`
|
EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"`
|
||||||
Address string `yaml:"Address"`
|
Address string `yaml:"Address"`
|
||||||
Port uint16 `yaml:"Port"`
|
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.
|
// NetMode describes the mode the blockchain will operate on.
|
||||||
|
|
|
@ -29,6 +29,7 @@ ProtocolConfiguration:
|
||||||
RegisterTransaction: 10000
|
RegisterTransaction: 10000
|
||||||
VerifyBlocks: true
|
VerifyBlocks: true
|
||||||
VerifyTransactions: false
|
VerifyTransactions: false
|
||||||
|
FreeGasLimit: 10.0
|
||||||
|
|
||||||
ApplicationConfiguration:
|
ApplicationConfiguration:
|
||||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||||
|
|
|
@ -29,6 +29,7 @@ ProtocolConfiguration:
|
||||||
RegisterTransaction: 100
|
RegisterTransaction: 100
|
||||||
VerifyBlocks: true
|
VerifyBlocks: true
|
||||||
VerifyTransactions: false
|
VerifyTransactions: false
|
||||||
|
FreeGasLimit: 10.0
|
||||||
|
|
||||||
ApplicationConfiguration:
|
ApplicationConfiguration:
|
||||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||||
|
|
|
@ -514,6 +514,11 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
|
||||||
v := bc.spawnVMWithInterops(systemInterop)
|
v := bc.spawnVMWithInterops(systemInterop)
|
||||||
v.SetCheckedHash(tx.VerificationHash().BytesBE())
|
v.SetCheckedHash(tx.VerificationHash().BytesBE())
|
||||||
v.LoadScript(t.Script)
|
v.LoadScript(t.Script)
|
||||||
|
v.SetPriceGetter(getPrice)
|
||||||
|
if bc.config.FreeGasLimit >= 0 {
|
||||||
|
v.SetGasLimit(bc.config.FreeGasLimit + t.Gas)
|
||||||
|
}
|
||||||
|
|
||||||
err := v.Run()
|
err := v.Run()
|
||||||
if !v.HasFailed() {
|
if !v.HasFailed() {
|
||||||
_, err := systemInterop.dao.Persist()
|
_, err := systemInterop.dao.Persist()
|
||||||
|
@ -554,7 +559,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
|
||||||
TxHash: tx.Hash(),
|
TxHash: tx.Hash(),
|
||||||
Trigger: trigger.Application,
|
Trigger: trigger.Application,
|
||||||
VMState: v.State(),
|
VMState: v.State(),
|
||||||
GasConsumed: util.Fixed8(0),
|
GasConsumed: v.GasConsumed(),
|
||||||
Stack: v.Stack("estack"),
|
Stack: v.Stack("estack"),
|
||||||
Events: systemInterop.notifications,
|
Events: systemInterop.notifications,
|
||||||
}
|
}
|
||||||
|
@ -1377,6 +1382,7 @@ func (bc *Blockchain) GetTestVM() (*vm.VM, storage.Store) {
|
||||||
tmpStore := storage.NewMemCachedStore(bc.dao.store)
|
tmpStore := storage.NewMemCachedStore(bc.dao.store)
|
||||||
systemInterop := bc.newInteropContext(trigger.Application, tmpStore, nil, nil)
|
systemInterop := bc.newInteropContext(trigger.Application, tmpStore, nil, nil)
|
||||||
vm := bc.spawnVMWithInterops(systemInterop)
|
vm := bc.spawnVMWithInterops(systemInterop)
|
||||||
|
vm.SetPriceGetter(getPrice)
|
||||||
return vm, tmpStore
|
return vm, tmpStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
112
pkg/core/gas_price.go
Normal file
112
pkg/core/gas_price.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
118
pkg/core/gas_price_test.go
Normal file
118
pkg/core/gas_price_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
|
@ -396,11 +396,12 @@ func (s *Server) invokescript(reqParams Params) (interface{}, error) {
|
||||||
// result.
|
// result.
|
||||||
func (s *Server) runScriptInVM(script []byte) *wrappers.InvokeResult {
|
func (s *Server) runScriptInVM(script []byte) *wrappers.InvokeResult {
|
||||||
vm, _ := s.chain.GetTestVM()
|
vm, _ := s.chain.GetTestVM()
|
||||||
|
vm.SetGasLimit(s.config.MaxGasInvoke)
|
||||||
vm.LoadScript(script)
|
vm.LoadScript(script)
|
||||||
_ = vm.Run()
|
_ = vm.Run()
|
||||||
result := &wrappers.InvokeResult{
|
result := &wrappers.InvokeResult{
|
||||||
State: vm.State(),
|
State: vm.State(),
|
||||||
GasConsumed: "0.1",
|
GasConsumed: vm.GasConsumed().String(),
|
||||||
Script: hex.EncodeToString(script),
|
Script: hex.EncodeToString(script),
|
||||||
Stack: vm.Estack(),
|
Stack: vm.Estack(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,8 +87,20 @@ func Fixed8FromString(s string) (Fixed8, error) {
|
||||||
|
|
||||||
// UnmarshalJSON implements the json unmarshaller interface.
|
// UnmarshalJSON implements the json unmarshaller interface.
|
||||||
func (f *Fixed8) UnmarshalJSON(data []byte) error {
|
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
|
var s string
|
||||||
if err := json.Unmarshal(data, &s); err == nil {
|
if err := unmarshal(&s); err == nil {
|
||||||
p, err := Fixed8FromString(s)
|
p, err := Fixed8FromString(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -98,7 +110,7 @@ func (f *Fixed8) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var fl float64
|
var fl float64
|
||||||
if err := json.Unmarshal(data, &fl); err != nil {
|
if err := unmarshal(&fl); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +123,11 @@ func (f Fixed8) MarshalJSON() ([]byte, error) {
|
||||||
return []byte(`"` + f.String() + `"`), nil
|
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.
|
// DecodeBinary implements the io.Serializable interface.
|
||||||
func (f *Fixed8) DecodeBinary(r *io.BinReader) {
|
func (f *Fixed8) DecodeBinary(r *io.BinReader) {
|
||||||
*f = Fixed8(r.ReadU64LE())
|
*f = Fixed8(r.ReadU64LE())
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/CityOfZion/neo-go/pkg/io"
|
"github.com/CityOfZion/neo-go/pkg/io"
|
||||||
|
"github.com/go-yaml/yaml"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -125,6 +126,19 @@ func TestFixed8_MarshalJSON(t *testing.T) {
|
||||||
assert.Equal(t, []byte(`"123.4"`), s)
|
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) {
|
func TestFixed8_Arith(t *testing.T) {
|
||||||
u1 := Fixed8FromInt64(3)
|
u1 := Fixed8FromInt64(3)
|
||||||
u2 := Fixed8FromInt64(8)
|
u2 := Fixed8FromInt64(8)
|
||||||
|
|
121
pkg/vm/vm.go
121
pkg/vm/vm.go
|
@ -63,6 +63,9 @@ type VM struct {
|
||||||
// callbacks to get interops.
|
// callbacks to get interops.
|
||||||
getInterop []InteropGetterFunc
|
getInterop []InteropGetterFunc
|
||||||
|
|
||||||
|
// callback to get interop price
|
||||||
|
getPrice func(*VM, opcode.Opcode, []byte) util.Fixed8
|
||||||
|
|
||||||
// callback to get scripts.
|
// callback to get scripts.
|
||||||
getScript func(util.Uint160) []byte
|
getScript func(util.Uint160) []byte
|
||||||
|
|
||||||
|
@ -76,6 +79,9 @@ type VM struct {
|
||||||
itemCount map[StackItem]int
|
itemCount map[StackItem]int
|
||||||
size int
|
size int
|
||||||
|
|
||||||
|
gasConsumed util.Fixed8
|
||||||
|
gasLimit util.Fixed8
|
||||||
|
|
||||||
// Public keys cache.
|
// Public keys cache.
|
||||||
keys map[string]*keys.PublicKey
|
keys map[string]*keys.PublicKey
|
||||||
}
|
}
|
||||||
|
@ -114,6 +120,23 @@ func (v *VM) RegisterInteropGetter(f InteropGetterFunc) {
|
||||||
v.getInterop = append(v.getInterop, f)
|
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.
|
// Estack returns the evaluation stack so interop hooks can utilize this.
|
||||||
func (v *VM) Estack() *Stack {
|
func (v *VM) Estack() *Stack {
|
||||||
return v.estack
|
return v.estack
|
||||||
|
@ -225,6 +248,7 @@ func (v *VM) Load(prog []byte) {
|
||||||
v.estack.Clear()
|
v.estack.Clear()
|
||||||
v.astack.Clear()
|
v.astack.Clear()
|
||||||
v.state = noneState
|
v.state = noneState
|
||||||
|
v.gasConsumed = 0
|
||||||
v.LoadScript(prog)
|
v.LoadScript(prog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,6 +474,27 @@ func (v *VM) SetScriptGetter(gs func(util.Uint160) []byte) {
|
||||||
v.getScript = gs
|
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).
|
// 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) {
|
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
|
// 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 {
|
if op >= opcode.PUSHBYTES1 && op <= opcode.PUSHBYTES75 {
|
||||||
v.estack.PushVal(parameter)
|
v.estack.PushVal(parameter)
|
||||||
return
|
return
|
||||||
|
@ -1053,51 +1105,28 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
|
||||||
v.estack.PushVal(len(arr))
|
v.estack.PushVal(len(arr))
|
||||||
|
|
||||||
case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT:
|
case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT:
|
||||||
var (
|
offset := v.getJumpOffset(ctx, parameter)
|
||||||
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))
|
|
||||||
}
|
|
||||||
cond := true
|
cond := true
|
||||||
if op > opcode.JMP {
|
if op != opcode.JMP {
|
||||||
cond = v.estack.Pop().Bool()
|
cond = v.estack.Pop().Bool() == (op == opcode.JMPIF)
|
||||||
if op == opcode.JMPIFNOT {
|
|
||||||
cond = !cond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cond {
|
|
||||||
ctx.nextip = offset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.jumpIf(ctx, offset, cond)
|
||||||
|
|
||||||
case opcode.CALL:
|
case opcode.CALL:
|
||||||
v.checkInvocationStackSize()
|
v.checkInvocationStackSize()
|
||||||
|
|
||||||
newCtx := ctx.Copy()
|
newCtx := ctx.Copy()
|
||||||
newCtx.rvcount = -1
|
newCtx.rvcount = -1
|
||||||
v.istack.PushVal(newCtx)
|
v.istack.PushVal(newCtx)
|
||||||
err = v.execute(v.Context(), opcode.JMP, parameter)
|
|
||||||
if err != nil {
|
offset := v.getJumpOffset(newCtx, parameter)
|
||||||
return
|
v.jumpIf(newCtx, offset, true)
|
||||||
}
|
|
||||||
|
|
||||||
case opcode.SYSCALL:
|
case opcode.SYSCALL:
|
||||||
var ifunc *InteropFuncPrice
|
interopID := GetInteropID(parameter)
|
||||||
var interopID uint32
|
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 {
|
if ifunc == nil {
|
||||||
panic(fmt.Sprintf("interop hook (%q/0x%x) not registered", parameter, interopID))
|
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.estack = newCtx.estack
|
||||||
v.astack = newCtx.astack
|
v.astack = newCtx.astack
|
||||||
if op == opcode.CALLI {
|
if op == opcode.CALLI {
|
||||||
err = v.execute(v.Context(), opcode.JMP, parameter[2:])
|
offset := v.getJumpOffset(newCtx, parameter[2:])
|
||||||
if err != nil {
|
v.jumpIf(newCtx, offset, true)
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case opcode.THROW:
|
case opcode.THROW:
|
||||||
|
@ -1381,6 +1408,26 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
|
||||||
return
|
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 {
|
func cloneIfStruct(item StackItem) StackItem {
|
||||||
switch it := item.(type) {
|
switch it := item.(type) {
|
||||||
case *StructItem:
|
case *StructItem:
|
||||||
|
|
|
@ -62,6 +62,54 @@ func TestRegisterInteropGetter(t *testing.T) {
|
||||||
assert.Equal(t, currRegistered+1, len(v.getInterop))
|
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) {
|
func TestBytesToPublicKey(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
cache := v.GetPublicKeys()
|
cache := v.GetPublicKeys()
|
||||||
|
|
Loading…
Reference in a new issue