From ceca9cdb67e1fc38d5b76d1289054c5144162d0c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 30 Sep 2019 19:52:16 +0300 Subject: [PATCH 01/12] core/vm: implement contract storage and script retrieval Fixes script invocations via the APPCALL instruction. Adjust contract state field types accordingly. --- pkg/core/blockchain.go | 40 +++++++++++++++- pkg/core/contract_state.go | 74 +++++++++++++++++++++++++++++- pkg/core/contract_state_test.go | 39 ++++++++++++++++ pkg/smartcontract/param_context.go | 2 +- pkg/vm/vm.go | 29 +++++++----- pkg/vm/vm_test.go | 7 ++- 6 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 pkg/core/contract_state_test.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3f955fd06..59e4bc5b5 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -313,6 +313,7 @@ func (bc *Blockchain) storeBlock(block *Block) error { spentCoins = make(SpentCoins) accounts = make(Accounts) assets = make(Assets) + contracts = make(Contracts) ) if err := storeAsBlock(batch, block, 0); err != nil { @@ -399,7 +400,7 @@ func (bc *Blockchain) storeBlock(block *Block) error { Email: t.Email, Description: t.Description, } - _ = contract + contracts[contract.ScriptHash()] = contract case *transaction.InvocationTX: } @@ -418,6 +419,9 @@ func (bc *Blockchain) storeBlock(block *Block) error { if err := assets.commit(batch); err != nil { return err } + if err := contracts.commit(batch); err != nil { + return err + } if err := bc.memStore.PutBatch(batch); err != nil { return err } @@ -643,6 +647,33 @@ func getAssetStateFromStore(s storage.Store, assetID util.Uint256) *AssetState { return &a } +// GetContractState returns contract by its script hash. +func (bc *Blockchain) GetContractState(hash util.Uint160) *ContractState { + cs := getContractStateFromStore(bc.memStore, hash) + if cs == nil { + cs = getContractStateFromStore(bc.Store, hash) + } + return cs +} + +// getContractStateFromStore returns contract state as recorded in the given +// store by the given script hash. +func getContractStateFromStore(s storage.Store, hash util.Uint160) *ContractState { + key := storage.AppendPrefix(storage.STContract, hash.Bytes()) + contractBytes, err := s.Get(key) + if err != nil { + return nil + } + var c ContractState + r := io.NewBinReaderFromBuf(contractBytes) + c.DecodeBinary(r) + if r.Err != nil || c.ScriptHash() != hash { + return nil + } + + return &c +} + // GetAccountState returns the account state from its script hash func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState { as, err := getAccountStateFromStore(bc.memStore, scriptHash) @@ -1001,6 +1032,13 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error { vm := vm.New(vm.ModeMute) vm.SetCheckedHash(t.VerificationHash().Bytes()) + vm.SetScriptGetter(func(hash util.Uint160) []byte { + cs := bc.GetContractState(hash) + if cs == nil { + return nil + } + return cs.Script + }) vm.LoadScript(verification) vm.LoadScript(witnesses[i].InvocationScript) vm.Run() diff --git a/pkg/core/contract_state.go b/pkg/core/contract_state.go index f33946d26..1ba19cbaa 100644 --- a/pkg/core/contract_state.go +++ b/pkg/core/contract_state.go @@ -1,16 +1,22 @@ package core import ( + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" ) +// Contracts is a mapping between scripthash and ContractState. +type Contracts map[util.Uint160]*ContractState + // ContractState holds information about a smart contract in the NEO blockchain. type ContractState struct { Script []byte ParamList []smartcontract.ParamType ReturnType smartcontract.ParamType - Properties []int + Properties []byte Name string CodeVersion string Author string @@ -21,3 +27,69 @@ type ContractState struct { scriptHash util.Uint160 } + +// commit flushes all contracts to the given storage.Batch. +func (a Contracts) commit(b storage.Batch) error { + buf := io.NewBufBinWriter() + for hash, contract := range a { + contract.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + key := storage.AppendPrefix(storage.STContract, hash.Bytes()) + b.Put(key, buf.Bytes()) + buf.Reset() + } + return nil +} + +// DecodeBinary implements Serializable interface. +func (a *ContractState) DecodeBinary(br *io.BinReader) { + a.Script = br.ReadBytes() + paramBytes := br.ReadBytes() + a.ParamList = make([]smartcontract.ParamType, len(paramBytes)) + for k := range paramBytes { + a.ParamList[k] = smartcontract.ParamType(paramBytes[k]) + } + br.ReadLE(&a.ReturnType) + a.Properties = br.ReadBytes() + a.Name = br.ReadString() + a.CodeVersion = br.ReadString() + a.Author = br.ReadString() + a.Email = br.ReadString() + a.Description = br.ReadString() + br.ReadLE(&a.HasStorage) + br.ReadLE(&a.HasDynamicInvoke) + a.createHash() +} + +// EncodeBinary implements Serializable interface. +func (a *ContractState) EncodeBinary(bw *io.BinWriter) { + bw.WriteBytes(a.Script) + bw.WriteVarUint(uint64(len(a.ParamList))) + for k := range a.ParamList { + bw.WriteLE(a.ParamList[k]) + } + bw.WriteLE(a.ReturnType) + bw.WriteBytes(a.Properties) + bw.WriteString(a.Name) + bw.WriteString(a.CodeVersion) + bw.WriteString(a.Author) + bw.WriteString(a.Email) + bw.WriteString(a.Description) + bw.WriteLE(a.HasStorage) + bw.WriteLE(a.HasDynamicInvoke) +} + +// ScriptHash returns a contract script hash. +func (a *ContractState) ScriptHash() util.Uint160 { + if a.scriptHash.Equals(util.Uint160{}) { + a.createHash() + } + return a.scriptHash +} + +// createHash creates contract script hash. +func (a *ContractState) createHash() { + a.scriptHash = hash.Hash160(a.Script) +} diff --git a/pkg/core/contract_state_test.go b/pkg/core/contract_state_test.go new file mode 100644 index 000000000..a34039aae --- /dev/null +++ b/pkg/core/contract_state_test.go @@ -0,0 +1,39 @@ +package core + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeContractState(t *testing.T) { + script := []byte("testscript") + + contract := &ContractState{ + Script: script, + ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type}, + ReturnType: smartcontract.BoolType, + Properties: []byte("smth"), + Name: "Contracto", + CodeVersion: "1.0.0", + Author: "Joe Random", + Email: "joe@example.com", + Description: "Test contract", + HasStorage: true, + HasDynamicInvoke: false, + } + + assert.Equal(t, hash.Hash160(script), contract.ScriptHash()) + buf := io.NewBufBinWriter() + contract.EncodeBinary(buf.BinWriter) + assert.Nil(t, buf.Err) + contractDecoded := &ContractState{} + r := io.NewBinReaderFromBuf(buf.Bytes()) + contractDecoded.DecodeBinary(r) + assert.Nil(t, r.Err) + assert.Equal(t, contract, contractDecoded) + assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash()) +} diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index b23d95b2d..b4b75c45c 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -3,7 +3,7 @@ package smartcontract import "github.com/CityOfZion/neo-go/pkg/util" // ParamType represent the Type of the contract parameter -type ParamType int +type ParamType byte // A list of supported smart contract parameter types. const ( diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 49f20259b..5754a7436 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -35,8 +35,8 @@ type VM struct { // registered interop hooks. interop map[string]InteropFunc - // scripts loaded in memory. - scripts map[util.Uint160][]byte + // callback to get scripts. + getScript func(util.Uint160) []byte istack *Stack // invocation stack. estack *Stack // execution stack. @@ -51,12 +51,12 @@ type VM struct { // New returns a new VM object ready to load .avm bytecode scripts. func New(mode Mode) *VM { vm := &VM{ - interop: make(map[string]InteropFunc), - scripts: make(map[util.Uint160][]byte), - state: haltState, - istack: NewStack("invocation"), - estack: NewStack("evaluation"), - astack: NewStack("alt"), + interop: make(map[string]InteropFunc), + getScript: nil, + state: haltState, + istack: NewStack("invocation"), + estack: NewStack("evaluation"), + astack: NewStack("alt"), } if mode == ModeMute { vm.mute = true @@ -248,6 +248,11 @@ func (v *VM) SetCheckedHash(h []byte) { copy(v.checkhash, h) } +// SetScriptGetter sets the script getter for CALL instructions. +func (v *VM) SetScriptGetter(gs func(util.Uint160) []byte) { + v.getScript = gs +} + // execute performs an instruction cycle in the VM. Acting on the instruction (opcode). func (v *VM) execute(ctx *Context, op Instruction) { // Instead of polluting the whole VM logic with error handling, we will recover @@ -850,8 +855,8 @@ func (v *VM) execute(ctx *Context, op Instruction) { } case APPCALL, TAILCALL: - if len(v.scripts) == 0 { - panic("script table is empty") + if v.getScript == nil { + panic("no getScript callback is set up") } hash, err := util.Uint160DecodeBytes(ctx.readBytes(20)) @@ -859,8 +864,8 @@ func (v *VM) execute(ctx *Context, op Instruction) { panic(err) } - script, ok := v.scripts[hash] - if !ok { + script := v.getScript(hash) + if script == nil { panic("could not find script") } diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 428a24735..b1cc8cd65 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -1000,7 +1000,12 @@ func TestAppCall(t *testing.T) { prog = append(prog, byte(RET)) vm := load(prog) - vm.scripts[hash] = makeProgram(DEPTH) + vm.SetScriptGetter(func(in util.Uint160) []byte { + if in.Equals(hash) { + return makeProgram(DEPTH) + } + return nil + }) vm.estack.PushVal(2) vm.Run() From 26e3b6abbe827613f9f616c644dd11e20e40c162 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 1 Oct 2019 16:37:42 +0300 Subject: [PATCH 02/12] vm: extend interops to contain price The same way C# node does. --- pkg/vm/tests/vm_test.go | 6 +++--- pkg/vm/vm.go | 20 +++++++++++++------- pkg/vm/vm_test.go | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pkg/vm/tests/vm_test.go b/pkg/vm/tests/vm_test.go index 5ace8c269..885e3ebc6 100644 --- a/pkg/vm/tests/vm_test.go +++ b/pkg/vm/tests/vm_test.go @@ -45,9 +45,9 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { vm := vm.New(vm.ModeMute) storePlugin := newStoragePlugin() - vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get) - vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put) - vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext) + vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get, 1) + vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put, 1) + vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext, 1) b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{}) if err != nil { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 5754a7436..310e18b6f 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -33,7 +33,7 @@ type VM struct { state State // registered interop hooks. - interop map[string]InteropFunc + interop map[string]InteropFuncPrice // callback to get scripts. getScript func(util.Uint160) []byte @@ -48,10 +48,16 @@ type VM struct { checkhash []byte } +// InteropFuncPrice represents an interop function with a price. +type InteropFuncPrice struct { + Func InteropFunc + Price int +} + // New returns a new VM object ready to load .avm bytecode scripts. func New(mode Mode) *VM { vm := &VM{ - interop: make(map[string]InteropFunc), + interop: make(map[string]InteropFuncPrice), getScript: nil, state: haltState, istack: NewStack("invocation"), @@ -63,15 +69,15 @@ func New(mode Mode) *VM { } // Register native interop hooks. - vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog) - vm.RegisterInteropFunc("Neo.Runtime.Notify", runtimeNotify) + vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog, 1) + vm.RegisterInteropFunc("Neo.Runtime.Notify", runtimeNotify, 1) return vm } // RegisterInteropFunc will register the given InteropFunc to the VM. -func (v *VM) RegisterInteropFunc(name string, f InteropFunc) { - v.interop[name] = f +func (v *VM) RegisterInteropFunc(name string, f InteropFunc, price int) { + v.interop[name] = InteropFuncPrice{f, price} } // Estack will return the evaluation stack so interop hooks can utilize this. @@ -850,7 +856,7 @@ func (v *VM) execute(ctx *Context, op Instruction) { if !ok { panic(fmt.Sprintf("interop hook (%s) not registered", api)) } - if err := ifunc(v); err != nil { + if err := ifunc.Func(v); err != nil { panic(fmt.Sprintf("failed to invoke syscall: %s", err)) } diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index b1cc8cd65..7e11eb7c5 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -18,7 +18,7 @@ func TestInteropHook(t *testing.T) { v.RegisterInteropFunc("foo", func(evm *VM) error { evm.Estack().PushVal(1) return nil - }) + }, 1) buf := new(bytes.Buffer) EmitSyscall(buf, "foo") @@ -33,7 +33,7 @@ func TestInteropHook(t *testing.T) { func TestRegisterInterop(t *testing.T) { v := New(ModeMute) currRegistered := len(v.interop) - v.RegisterInteropFunc("foo", func(evm *VM) error { return nil }) + v.RegisterInteropFunc("foo", func(evm *VM) error { return nil }, 1) assert.Equal(t, currRegistered+1, len(v.interop)) _, ok := v.interop["foo"] assert.Equal(t, true, ok) From da2156f95564b17d99f3bda71e8a0467ca6a140c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 1 Oct 2019 16:38:33 +0300 Subject: [PATCH 03/12] vm: add batched RegisterInteropFuncs --- pkg/vm/vm.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 310e18b6f..6ecfbd9e4 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -80,6 +80,15 @@ func (v *VM) RegisterInteropFunc(name string, f InteropFunc, price int) { v.interop[name] = InteropFuncPrice{f, price} } +// RegisterInteropFuncs will register all interop functions passed in a map in +// the VM. Effectively it's a batched version of RegisterInteropFunc. +func (v *VM) RegisterInteropFuncs(interops map[string]InteropFuncPrice) { + // We allow reregistration here. + for name, funPrice := range interops { + v.interop[name] = funPrice + } +} + // Estack will return the evaluation stack so interop hooks can utilize this. func (v *VM) Estack() *Stack { return v.estack From cfa0c133229ef7cc7610b54949eaa53fa54fe40a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 1 Oct 2019 20:11:01 +0300 Subject: [PATCH 04/12] vm: add InteropItem type for interop data This is an opaque data item that is to be used by the interop functions. --- pkg/vm/stack.go | 2 ++ pkg/vm/stack_item.go | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index e0987637d..86f652166 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -92,6 +92,8 @@ func (e *Element) Bool() bool { } } return false + case *InteropItem: + return t.value != nil default: panic("can't convert to bool: " + t.String()) } diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 9389178f8..b00d32383 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -248,3 +248,30 @@ func toMapKey(key StackItem) interface{} { panic("wrong key type") } } + +// InteropItem represents interop data on the stack. +type InteropItem struct { + value interface{} +} + +// NewInteropItem returns new InteropItem object. +func NewInteropItem(value interface{}) *InteropItem { + return &InteropItem{ + value: value, + } +} + +// Value implements StackItem interface. +func (i *InteropItem) Value() interface{} { + return i.value +} + +// String implements stringer interface. +func (i *InteropItem) String() string { + return "InteropItem" +} + +// MarshalJSON implements the json.Marshaler interface. +func (i *InteropItem) MarshalJSON() ([]byte, error) { + return json.Marshal(i.value) +} From a357d99624f45b2c2d1b426663ba836dab0ac950 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 15:58:52 +0300 Subject: [PATCH 05/12] vm: introduce MaxArraySize constant This is both for #373 and for interop functions that have to check some inputs. --- pkg/vm/vm.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 6ecfbd9e4..a12516ee3 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -24,8 +24,10 @@ var ( ) const ( - maxSHLArg = 256 - minSHLArg = -256 + // MaxArraySize is the maximum array size allowed in the VM. + MaxArraySize = 1024 + maxSHLArg = 256 + minSHLArg = -256 ) // VM represents the virtual machine. From 705c7f106fd745f356eb4060905905e32084875d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:02:15 +0300 Subject: [PATCH 06/12] vm: don't panic if there is no result in PopResult() This function is intended to be ran outside of the execute's panic recovery mechanism, so it shouldn't panic if there is no result. --- pkg/vm/vm.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index a12516ee3..cb3448570 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -187,7 +187,11 @@ func (v *VM) Context() *Context { // PopResult is used to pop the first item of the evaluation stack. This allows // us to test compiler and vm in a bi-directional way. func (v *VM) PopResult() interface{} { - return v.estack.Pop().value.Value() + e := v.estack.Pop() + if e != nil { + return e.Value() + } + return nil } // Stack returns json formatted representation of the given stack. From 0c963875af1e824ff04004bd67f183eab1808ad7 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:04:02 +0300 Subject: [PATCH 07/12] vm: check for fault flag first in Run() Switch cases are evaluated sequentially and the fault case is top-priority to handle. --- pkg/vm/vm.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index cb3448570..7d534d667 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -225,6 +225,9 @@ func (v *VM) Run() { v.state = noneState for { switch { + case v.state.HasFlag(faultState): + fmt.Println("FAULT") + return case v.state.HasFlag(haltState): if !v.mute { fmt.Println(v.Stack("estack")) @@ -235,9 +238,6 @@ func (v *VM) Run() { i, op := ctx.CurrInstr() fmt.Printf("at breakpoint %d (%s)\n", i, op.String()) return - case v.state.HasFlag(faultState): - fmt.Println("FAULT") - return case v.state == noneState: v.Step() } From d62a367900022797852004be7bbb518bb049bdc9 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:12:24 +0300 Subject: [PATCH 08/12] vm: add Value() method to Element It gives access to the internal value's Value() which is essential for interop functions that need to get something from InteropItems. And it also simplifies some already existing code along the way. --- pkg/vm/interop.go | 4 ++-- pkg/vm/stack.go | 5 +++++ pkg/vm/vm.go | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/vm/interop.go b/pkg/vm/interop.go index e59d4ac78..5ca9d7884 100644 --- a/pkg/vm/interop.go +++ b/pkg/vm/interop.go @@ -10,13 +10,13 @@ type InteropFunc func(vm *VM) error // runtimeLog will handle the syscall "Neo.Runtime.Log" for printing and logging stuff. func runtimeLog(vm *VM) error { item := vm.Estack().Pop() - fmt.Printf("NEO-GO-VM (log) > %s\n", item.value.Value()) + fmt.Printf("NEO-GO-VM (log) > %s\n", item.Value()) return nil } // runtimeNotify will handle the syscall "Neo.Runtime.Notify" for printing and logging stuff. func runtimeNotify(vm *VM) error { item := vm.Estack().Pop() - fmt.Printf("NEO-GO-VM (notify) > %s\n", item.value.Value()) + fmt.Printf("NEO-GO-VM (notify) > %s\n", item.Value()) return nil } diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index 86f652166..c9ce195be 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -58,6 +58,11 @@ func (e *Element) Prev() *Element { return nil } +// Value returns value of the StackItem contained in the element. +func (e *Element) Value() interface{} { + return e.value.Value() +} + // BigInt attempts to get the underlying value of the element as a big integer. // Will panic if the assertion failed which will be caught by the VM. func (e *Element) BigInt() *big.Int { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 7d534d667..371688b82 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -181,7 +181,7 @@ func (v *VM) Context() *Context { if v.istack.Len() == 0 { return nil } - return v.istack.Peek(0).value.Value().(*Context) + return v.istack.Peek(0).Value().(*Context) } // PopResult is used to pop the first item of the evaluation stack. This allows @@ -827,7 +827,7 @@ func (v *VM) execute(ctx *Context, op Instruction) { elem := v.estack.Pop() // Cause there is no native (byte) item type here, hence we need to check // the type of the item for array size operations. - switch t := elem.value.Value().(type) { + switch t := elem.Value().(type) { case []StackItem: v.estack.PushVal(len(t)) case map[interface{}]StackItem: From 8441b31b4b364427a1971a4b28489c5765cf30c0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:23:21 +0300 Subject: [PATCH 09/12] vm: accept uint32 in makeStackItem() Interop services routinely push such things (block index, blockchain height) onto the stack. --- pkg/vm/stack_item.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index b00d32383..27ec00621 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -23,6 +23,10 @@ func makeStackItem(v interface{}) StackItem { return &BigIntegerItem{ value: big.NewInt(val), } + case uint32: + return &BigIntegerItem{ + value: big.NewInt(int64(val)), + } case []byte: return &ByteArrayItem{ value: val, From 1bf232ad50612464cc5cb1065459c124ae3572ae Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:26:32 +0300 Subject: [PATCH 10/12] vm: introduce TryBool() for Element and use it in VerifyWitnesses Script can return non-bool results that can still be converted to bool according to the usual VM rules. Unfortunately Bool() panics if this conversion fails which is OK for things done in vm.execute(), but certainly not for VerifyWitnesses(), thus there is a need for TryBool() that will just return an error in this case. --- pkg/core/blockchain.go | 15 +++++++++------ pkg/vm/stack.go | 30 ++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 59e4bc5b5..0365c2517 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1045,14 +1045,17 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error { if vm.HasFailed() { return errors.Errorf("vm failed to execute the script") } - res := vm.PopResult() - switch res.(type) { - case bool: - if !(res.(bool)) { + resEl := vm.Estack().Pop() + if resEl != nil { + res, err := resEl.TryBool() + if err != nil { + return err + } + if !res { return errors.Errorf("signature check failed") } - default: - return errors.Errorf("vm returned non-boolean result") + } else { + return errors.Errorf("no result returned from the script") } } diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index c9ce195be..5dd5b2dd1 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -80,30 +80,40 @@ func (e *Element) BigInt() *big.Int { } } -// Bool attempts to get the underlying value of the element as a boolean. -// Will panic if the assertion failed which will be caught by the VM. -func (e *Element) Bool() bool { +// TryBool attempts to get the underlying value of the element as a boolean. +// Returns error if can't convert value to boolean type. +func (e *Element) TryBool() (bool, error) { switch t := e.value.(type) { case *BigIntegerItem: - return t.value.Int64() != 0 + return t.value.Int64() != 0, nil case *BoolItem: - return t.value + return t.value, nil case *ArrayItem, *StructItem: - return true + return true, nil case *ByteArrayItem: for _, b := range t.value { if b != 0 { - return true + return true, nil } } - return false + return false, nil case *InteropItem: - return t.value != nil + return t.value != nil, nil default: - panic("can't convert to bool: " + t.String()) + return false, fmt.Errorf("can't convert to bool: " + t.String()) } } +// Bool attempts to get the underlying value of the element as a boolean. +// Will panic if the assertion failed which will be caught by the VM. +func (e *Element) Bool() bool { + val, err := e.TryBool() + if err != nil { + panic(err) + } + return val +} + // Bytes attempts to get the underlying value of the element as a byte array. // Will panic if the assertion failed which will be caught by the VM. func (e *Element) Bytes() []byte { From 53a3b18652d45311028e32c689f1f74641b0dd9d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:54:14 +0300 Subject: [PATCH 11/12] vm: completely separate instruction read and execution phases Make Context.Next() return both opcode and instruction parameter if any. This simplifies some code and needed to deal with #295. --- pkg/vm/context.go | 114 +++++++++++++++++++++++----------------------- pkg/vm/vm.go | 68 ++++++++++----------------- pkg/vm/vm_test.go | 2 +- 3 files changed, 82 insertions(+), 102 deletions(-) diff --git a/pkg/vm/context.go b/pkg/vm/context.go index bf4f061ee..1e7ac9d1a 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -1,7 +1,9 @@ package vm import ( - "encoding/binary" + "errors" + + "github.com/CityOfZion/neo-go/pkg/io" ) // Context represent the current execution context of the VM. @@ -9,6 +11,9 @@ type Context struct { // Instruction pointer. ip int + // The next instruction pointer. + nextip int + // The raw program script. prog []byte @@ -19,19 +24,62 @@ type Context struct { // NewContext return a new Context object. func NewContext(b []byte) *Context { return &Context{ - ip: -1, prog: b, breakPoints: []int{}, } } -// Next return the next instruction to execute. -func (c *Context) Next() Instruction { - c.ip++ +// Next returns the next instruction to execute with its parameter if any. After +// its invocation the instruction pointer points to the instruction being +// returned. +func (c *Context) Next() (Instruction, []byte, error) { + c.ip = c.nextip if c.ip >= len(c.prog) { - return RET + return RET, nil, nil } - return Instruction(c.prog[c.ip]) + r := io.NewBinReaderFromBuf(c.prog[c.ip:]) + + var instrbyte byte + r.ReadLE(&instrbyte) + instr := Instruction(instrbyte) + c.nextip++ + + var numtoread int + switch instr { + case PUSHDATA1, SYSCALL: + var n byte + r.ReadLE(&n) + numtoread = int(n) + c.nextip++ + case PUSHDATA2: + var n uint16 + r.ReadLE(&n) + numtoread = int(n) + c.nextip += 2 + case PUSHDATA4: + var n uint32 + r.ReadLE(&n) + numtoread = int(n) + c.nextip += 4 + case JMP, JMPIF, JMPIFNOT, CALL: + numtoread = 2 + case APPCALL, TAILCALL: + numtoread = 20 + default: + if instr >= PUSHBYTES1 && instr <= PUSHBYTES75 { + numtoread = int(instr) + } else { + // No parameters, can just return. + return instr, nil, nil + } + } + parameter := make([]byte, numtoread) + r.ReadLE(parameter) + if r.Err != nil { + return instr, nil, errors.New("failed to read instruction parameter") + } + c.nextip += numtoread + return instr, parameter, nil } // IP returns the absolute instruction without taking 0 into account. @@ -48,19 +96,14 @@ func (c *Context) LenInstr() int { // CurrInstr returns the current instruction and opcode. func (c *Context) CurrInstr() (int, Instruction) { - if c.ip < 0 { - return c.ip, NOP - } return c.ip, Instruction(c.prog[c.ip]) } // Copy returns an new exact copy of c. func (c *Context) Copy() *Context { - return &Context{ - ip: c.ip, - prog: c.prog, - breakPoints: c.breakPoints, - } + ctx := new(Context) + *ctx = *c + return ctx } // Program returns the loaded program. @@ -85,44 +128,3 @@ func (c *Context) atBreakPoint() bool { func (c *Context) String() string { return "execution context" } - -func (c *Context) readUint32() uint32 { - start, end := c.IP(), c.IP()+4 - if end > len(c.prog) { - panic("failed to read uint32 parameter") - } - val := binary.LittleEndian.Uint32(c.prog[start:end]) - c.ip += 4 - return val -} - -func (c *Context) readUint16() uint16 { - start, end := c.IP(), c.IP()+2 - if end > len(c.prog) { - panic("failed to read uint16 parameter") - } - val := binary.LittleEndian.Uint16(c.prog[start:end]) - c.ip += 2 - return val -} - -func (c *Context) readByte() byte { - return c.readBytes(1)[0] -} - -func (c *Context) readBytes(n int) []byte { - start, end := c.IP(), c.IP()+n - if end > len(c.prog) { - return nil - } - - out := make([]byte, n) - copy(out, c.prog[start:end]) - c.ip += n - return out -} - -func (c *Context) readVarBytes() []byte { - n := c.readByte() - return c.readBytes(int(n)) -} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 371688b82..2a698febb 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -2,6 +2,7 @@ package vm import ( "crypto/sha1" + "encoding/binary" "fmt" "io/ioutil" "log" @@ -224,6 +225,11 @@ func (v *VM) Run() { v.state = noneState for { + // check for breakpoint before executing the next instruction + ctx := v.Context() + if ctx != nil && ctx.atBreakPoint() { + v.state |= breakState + } switch { case v.state.HasFlag(faultState): fmt.Println("FAULT") @@ -247,14 +253,13 @@ func (v *VM) Run() { // Step 1 instruction in the program. func (v *VM) Step() { ctx := v.Context() - op := ctx.Next() - v.execute(ctx, op) - - // re-peek the context as it could been changed during execution. - cctx := v.Context() - if cctx != nil && cctx.atBreakPoint() { - v.state = breakState + op, param, err := ctx.Next() + if err != nil { + log.Printf("error encountered at instruction %d (%s)", ctx.ip, op) + log.Println(err) + v.state = faultState } + v.execute(ctx, op, param) } // HasFailed returns whether VM is in the failed state now. Usually used to @@ -275,7 +280,7 @@ func (v *VM) SetScriptGetter(gs func(util.Uint160) []byte) { } // execute performs an instruction cycle in the VM. Acting on the instruction (opcode). -func (v *VM) execute(ctx *Context, op Instruction) { +func (v *VM) execute(ctx *Context, op Instruction, parameter []byte) { // Instead of polluting the whole VM logic with error handling, we will recover // each panic at a central point, putting the VM in a fault state. defer func() { @@ -287,11 +292,7 @@ func (v *VM) execute(ctx *Context, op Instruction) { }() if op >= PUSHBYTES1 && op <= PUSHBYTES75 { - b := ctx.readBytes(int(op)) - if b == nil { - panic("failed to read instruction parameter") - } - v.estack.PushVal(b) + v.estack.PushVal(parameter) return } @@ -305,29 +306,8 @@ func (v *VM) execute(ctx *Context, op Instruction) { case PUSH0: v.estack.PushVal([]byte{}) - case PUSHDATA1: - n := ctx.readByte() - b := ctx.readBytes(int(n)) - if b == nil { - panic("failed to read instruction parameter") - } - v.estack.PushVal(b) - - case PUSHDATA2: - n := ctx.readUint16() - b := ctx.readBytes(int(n)) - if b == nil { - panic("failed to read instruction parameter") - } - v.estack.PushVal(b) - - case PUSHDATA4: - n := ctx.readUint32() - b := ctx.readBytes(int(n)) - if b == nil { - panic("failed to read instruction parameter") - } - v.estack.PushVal(b) + case PUSHDATA1, PUSHDATA2, PUSHDATA4: + v.estack.PushVal(parameter) // Stack operations. case TOALTSTACK: @@ -843,8 +823,8 @@ func (v *VM) execute(ctx *Context, op Instruction) { case JMP, JMPIF, JMPIFNOT: var ( - rOffset = int16(ctx.readUint16()) - offset = ctx.ip + int(rOffset) - 3 // sizeOf(int16 + uint8) + 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)) @@ -857,19 +837,17 @@ func (v *VM) execute(ctx *Context, op Instruction) { } } if cond { - ctx.ip = offset + ctx.nextip = offset } case CALL: v.istack.PushVal(ctx.Copy()) - ctx.ip += 2 - v.execute(v.Context(), JMP) + v.execute(v.Context(), JMP, parameter) case SYSCALL: - api := ctx.readVarBytes() - ifunc, ok := v.interop[string(api)] + ifunc, ok := v.interop[string(parameter)] if !ok { - panic(fmt.Sprintf("interop hook (%s) not registered", api)) + panic(fmt.Sprintf("interop hook (%q) not registered", parameter)) } if err := ifunc.Func(v); err != nil { panic(fmt.Sprintf("failed to invoke syscall: %s", err)) @@ -880,7 +858,7 @@ func (v *VM) execute(ctx *Context, op Instruction) { panic("no getScript callback is set up") } - hash, err := util.Uint160DecodeBytes(ctx.readBytes(20)) + hash, err := util.Uint160DecodeBytes(parameter) if err != nil { panic(err) } diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 7e11eb7c5..e14f08dad 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -54,7 +54,7 @@ func TestPushBytes1to75(t *testing.T) { assert.IsType(t, elem.Bytes(), b) assert.Equal(t, 0, vm.estack.Len()) - vm.execute(nil, RET) + vm.execute(nil, RET, nil) assert.Equal(t, 0, vm.astack.Len()) assert.Equal(t, 0, vm.istack.Len()) From dca332f333666d1e3cdf32ede475455201a10b97 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 3 Oct 2019 16:56:48 +0300 Subject: [PATCH 12/12] vm: use new Context.Next() to properly dump programs Fix #295, deduplicate code and add `inspect` parameter to the vm command to dump AVMs (`contract inspect` works with Go code). --- cli/vm/vm.go | 32 ++++++++++++++++++++++++++ pkg/vm/compiler/compiler.go | 23 +++---------------- pkg/vm/vm.go | 45 +++++++++++++++++++++++++++++-------- 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/cli/vm/vm.go b/cli/vm/vm.go index 0bb55e1db..35a8f92c2 100644 --- a/cli/vm/vm.go +++ b/cli/vm/vm.go @@ -1,6 +1,10 @@ package vm import ( + "errors" + "io/ioutil" + + "github.com/CityOfZion/neo-go/pkg/vm" vmcli "github.com/CityOfZion/neo-go/pkg/vm/cli" "github.com/urfave/cli" ) @@ -14,6 +18,19 @@ func NewCommand() cli.Command { Flags: []cli.Flag{ cli.BoolFlag{Name: "debug, d"}, }, + Subcommands: []cli.Command{ + { + Name: "inspect", + Usage: "dump instructions of the avm file given", + Action: inspect, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "in, i", + Usage: "input file of the program (AVM)", + }, + }, + }, + }, } } @@ -21,3 +38,18 @@ func startVMPrompt(ctx *cli.Context) error { p := vmcli.New() return p.Run() } + +func inspect(ctx *cli.Context) error { + avm := ctx.String("in") + if len(avm) == 0 { + return cli.NewExitError(errors.New("no input file given"), 1) + } + b, err := ioutil.ReadFile(avm) + if err != nil { + return cli.NewExitError(err, 1) + } + v := vm.New(0) + v.LoadScript(b) + v.PrintOps() + return nil +} diff --git a/pkg/vm/compiler/compiler.go b/pkg/vm/compiler/compiler.go index e16da65e5..94bf31ee8 100644 --- a/pkg/vm/compiler/compiler.go +++ b/pkg/vm/compiler/compiler.go @@ -13,7 +13,6 @@ import ( "log" "os" "strings" - "text/tabwriter" "github.com/CityOfZion/neo-go/pkg/vm" "golang.org/x/tools/go/loader" @@ -108,25 +107,9 @@ func CompileAndInspect(src string) error { return err } - w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) - fmt.Fprintln(w, "INDEX\tOPCODE\tDESC\t") - for i := 0; i <= len(b)-1; { - instr := vm.Instruction(b[i]) - paramlength := 0 - fmt.Fprintf(w, "%d\t0x%x\t%s\t\n", i, b[i], instr) - i++ - if instr >= vm.PUSHBYTES1 && instr <= vm.PUSHBYTES75 { - paramlength = int(instr) - } - if instr == vm.JMP || instr == vm.JMPIF || instr == vm.JMPIFNOT || instr == vm.CALL { - paramlength = 2 - } - for x := 0; x < paramlength; x++ { - fmt.Fprintf(w, "%d\t0x%x\t%s\t\n", i, b[i+1+x], string(b[i+1+x])) - } - i += paramlength - } - w.Flush() + v := vm.New(0) + v.LoadScript(b) + v.PrintOps() return nil } diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 2a698febb..8262f6b0c 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -10,6 +10,7 @@ import ( "os" "reflect" "text/tabwriter" + "unicode/utf8" "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/keys" @@ -119,19 +120,45 @@ func (v *VM) LoadArgs(method []byte, args []StackItem) { // PrintOps will print the opcodes of the current loaded program to stdout. func (v *VM) PrintOps() { - prog := v.Context().Program() w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) - fmt.Fprintln(w, "INDEX\tOPCODE\tDESC\t") - cursor := "" - ip, _ := v.Context().CurrInstr() - for i := 0; i < len(prog); i++ { - if i == ip { + fmt.Fprintln(w, "INDEX\tOPCODE\tPARAMETER\t") + realctx := v.Context() + ctx := realctx.Copy() + ctx.ip = 0 + ctx.nextip = 0 + for { + cursor := "" + instr, parameter, err := ctx.Next() + if ctx.ip == realctx.ip { cursor = "<<" - } else { - cursor = "" } - fmt.Fprintf(w, "%d\t0x%2x\t%s\t%s\n", i, prog[i], Instruction(prog[i]).String(), cursor) + if err != nil { + fmt.Fprintf(w, "%d\t%s\tERROR: %s\t%s\n", ctx.ip, instr, err, cursor) + break + } + var desc = "" + if parameter != nil { + switch instr { + case JMP, JMPIF, JMPIFNOT, CALL: + offset := int16(binary.LittleEndian.Uint16(parameter)) + desc = fmt.Sprintf("%d (%d/%x)", ctx.ip+int(offset), offset, parameter) + case SYSCALL: + desc = fmt.Sprintf("%q", parameter) + case APPCALL, TAILCALL: + desc = fmt.Sprintf("%x", parameter) + default: + if utf8.Valid(parameter) { + desc = fmt.Sprintf("%x (%q)", parameter, parameter) + } else { + desc = fmt.Sprintf("%x", parameter) + } + } + } + fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", ctx.ip, instr, desc, cursor) + if ctx.nextip >= len(ctx.prog) { + break + } } w.Flush() }