Merge pull request #417 from nspcc-dev/various-verification-fixes2
Various verification fixes part 2, which focuses on VM improvements necessary to make tx verification work. It's mostly related to interop functionality, but doesn't add interops at the moment. Fixes #295 along the way.
This commit is contained in:
commit
aab2f9a837
13 changed files with 436 additions and 183 deletions
32
cli/vm/vm.go
32
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
|
||||
}
|
||||
|
|
|
@ -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,20 +1032,30 @@ 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()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
39
pkg/core/contract_state_test.go
Normal file
39
pkg/core/contract_state_test.go
Normal file
|
@ -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())
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -75,28 +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, 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 {
|
||||
|
|
|
@ -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,
|
||||
|
@ -248,3 +252,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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
191
pkg/vm/vm.go
191
pkg/vm/vm.go
|
@ -2,6 +2,7 @@ package vm
|
|||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -9,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"
|
||||
|
@ -24,8 +26,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.
|
||||
|
@ -33,10 +37,10 @@ type VM struct {
|
|||
state State
|
||||
|
||||
// registered interop hooks.
|
||||
interop map[string]InteropFunc
|
||||
interop map[string]InteropFuncPrice
|
||||
|
||||
// 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.
|
||||
|
@ -48,30 +52,45 @@ 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),
|
||||
scripts: make(map[util.Uint160][]byte),
|
||||
state: haltState,
|
||||
istack: NewStack("invocation"),
|
||||
estack: NewStack("evaluation"),
|
||||
astack: NewStack("alt"),
|
||||
interop: make(map[string]InteropFuncPrice),
|
||||
getScript: nil,
|
||||
state: haltState,
|
||||
istack: NewStack("invocation"),
|
||||
estack: NewStack("evaluation"),
|
||||
astack: NewStack("alt"),
|
||||
}
|
||||
if mode == ModeMute {
|
||||
vm.mute = true
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -101,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()
|
||||
}
|
||||
|
@ -164,13 +209,17 @@ 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
|
||||
// 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.
|
||||
|
@ -203,7 +252,15 @@ 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")
|
||||
return
|
||||
case v.state.HasFlag(haltState):
|
||||
if !v.mute {
|
||||
fmt.Println(v.Stack("estack"))
|
||||
|
@ -214,9 +271,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()
|
||||
}
|
||||
|
@ -226,14 +280,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
|
||||
|
@ -248,8 +301,13 @@ 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) {
|
||||
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() {
|
||||
|
@ -261,11 +319,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
|
||||
}
|
||||
|
||||
|
@ -279,29 +333,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:
|
||||
|
@ -801,7 +834,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:
|
||||
|
@ -817,8 +850,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))
|
||||
|
@ -831,36 +864,34 @@ 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(v); err != nil {
|
||||
if err := ifunc.Func(v); err != nil {
|
||||
panic(fmt.Sprintf("failed to invoke syscall: %s", err))
|
||||
}
|
||||
|
||||
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))
|
||||
hash, err := util.Uint160DecodeBytes(parameter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
script, ok := v.scripts[hash]
|
||||
if !ok {
|
||||
script := v.getScript(hash)
|
||||
if script == nil {
|
||||
panic("could not find script")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue