From c1b6738bdb17602633ab098e89c42978fee932c9 Mon Sep 17 00:00:00 2001 From: decentralisedkev <37423678+decentralisedkev@users.noreply.github.com> Date: Mon, 18 Mar 2019 21:40:21 +0000 Subject: [PATCH] VM: Add basic vm (#166) * VM:Add abstract stack item * VM: Add stackItems; Array, Boolean, Int and ByteArray * VM: Add tests for stack item * VM: first pass at Random Access Stack object * VM: Add Sub, Mul, Mod LSH, RSH * VM: moved test helper functions into separate file * VM: removed helper functions from stack_test.go * Add conversions for bytearray and Int stack items * Add instructions file for vm * - Add guide to stack readme - Add testReadInt64 * Add Builder * Refactor Int, Boolean, ByteArray conversion * Add Context stack Item * Add Invocation stack - convenience RAS * rename testhelper to test_helper * Move opcode file * - Add `Add` OpCode - Add Opcode Function map * - Add test for math `Add` opcode - basic opcode execution * Add popTwoIntegers convenience func * Add `SUB` Opcode * Export Context Read methods - Return errors where failable * - Add `Op` to handleOP func signature - Add PushNBytes OPcode * remove error on NewBoolean - Expose underlying with Getter on Boolean StackItem - Add Equals method for ByteArray * Make Next() method on Context failable, refactor peekContext and Peek * Add ExecuteOp, Step and Run methods on the VM * Add Equal Opcode * Add THROWIFNOT Opcode * Add RET Opcode * Refactor PushNBytes Opcode * refactor Add, Sub to return VMSTATE add popTwoByteArrays helper function * Add basic tests for vm * clarify vm states * Add astack * [VM] Pass ResultStack to the opcode handlers * [VM] refactor handlers to have rstack as argument * [Stack] - Change RemoveCurrentContext for PopCurrentContext - Add CopTo method to stack * [VM] Add Result stack Len check in simple run test * [VM] fix typo * [Peer/Stall] Change seconds to milliseconds in test --- pkg/peer/stall/stall_test.go | 16 +-- pkg/vm/stack/Int.go | 94 +++++++++++++++++ pkg/vm/stack/Readme.md | 24 +++++ pkg/vm/stack/array.go | 13 +++ pkg/vm/stack/boolean.go | 26 +++++ pkg/vm/stack/builder.go | 177 ++++++++++++++++++++++++++++++++ pkg/vm/stack/bytearray.go | 71 +++++++++++++ pkg/vm/stack/context.go | 152 +++++++++++++++++++++++++++ pkg/vm/stack/instruction.go | 133 ++++++++++++++++++++++++ pkg/vm/stack/int_test.go | 72 +++++++++++++ pkg/vm/stack/invocationstack.go | 61 +++++++++++ pkg/vm/stack/stack.go | 150 +++++++++++++++++++++++++++ pkg/vm/stack/stack_test.go | 161 +++++++++++++++++++++++++++++ pkg/vm/stack/stackitem.go | 49 +++++++++ pkg/vm/stack/stackitem_test.go | 68 ++++++++++++ pkg/vm/stack/test_helper.go | 44 ++++++++ pkg/vm/state.go | 20 ++++ pkg/vm/vm.go | 72 +++++++++++++ pkg/vm/vm_ops.go | 21 ++++ pkg/vm/vm_ops_bitwise.go | 17 +++ pkg/vm/vm_ops_exceptions.go | 33 ++++++ pkg/vm/vm_ops_flow.go | 27 +++++ pkg/vm/vm_ops_maths.go | 70 +++++++++++++ pkg/vm/vm_ops_maths_test.go | 69 +++++++++++++ pkg/vm/vm_ops_stackmani.go | 19 ++++ pkg/vm/vm_test.go | 119 +++++++++++++++++++++ 26 files changed, 1770 insertions(+), 8 deletions(-) create mode 100644 pkg/vm/stack/Int.go create mode 100644 pkg/vm/stack/Readme.md create mode 100644 pkg/vm/stack/array.go create mode 100644 pkg/vm/stack/boolean.go create mode 100644 pkg/vm/stack/builder.go create mode 100644 pkg/vm/stack/bytearray.go create mode 100644 pkg/vm/stack/context.go create mode 100644 pkg/vm/stack/instruction.go create mode 100644 pkg/vm/stack/int_test.go create mode 100644 pkg/vm/stack/invocationstack.go create mode 100644 pkg/vm/stack/stack.go create mode 100644 pkg/vm/stack/stack_test.go create mode 100644 pkg/vm/stack/stackitem.go create mode 100644 pkg/vm/stack/stackitem_test.go create mode 100644 pkg/vm/stack/test_helper.go create mode 100644 pkg/vm/state.go create mode 100644 pkg/vm/vm.go create mode 100644 pkg/vm/vm_ops.go create mode 100644 pkg/vm/vm_ops_bitwise.go create mode 100644 pkg/vm/vm_ops_exceptions.go create mode 100644 pkg/vm/vm_ops_flow.go create mode 100644 pkg/vm/vm_ops_maths.go create mode 100644 pkg/vm/vm_ops_maths_test.go create mode 100644 pkg/vm/vm_ops_stackmani.go create mode 100644 pkg/vm/vm_test.go diff --git a/pkg/peer/stall/stall_test.go b/pkg/peer/stall/stall_test.go index b86412b2a..4d5494e12 100644 --- a/pkg/peer/stall/stall_test.go +++ b/pkg/peer/stall/stall_test.go @@ -12,8 +12,8 @@ import ( func TestAddRemoveMessage(t *testing.T) { - responseTime := 2 * time.Second - tickerInterval := 1 * time.Second + responseTime := 2 * time.Millisecond + tickerInterval := 1 * time.Millisecond d := NewDetector(responseTime, tickerInterval) d.AddMessage(command.GetAddr) @@ -51,15 +51,15 @@ loop: } func TestDeadlineWorks(t *testing.T) { - responseTime := 2 * time.Second - tickerInterval := 1 * time.Second + responseTime := 2 * time.Millisecond + tickerInterval := 1 * time.Millisecond d := NewDetector(responseTime, tickerInterval) mp := mockPeer{online: true, detector: d, lock: new(sync.RWMutex)} go mp.loop() d.AddMessage(command.GetAddr) - time.Sleep(responseTime + 1*time.Second) + time.Sleep(responseTime + 1*time.Millisecond) k := make(map[command.Type]time.Time) d.lock.RLock() @@ -70,12 +70,12 @@ func TestDeadlineWorks(t *testing.T) { mp.lock.RUnlock() } func TestDeadlineShouldNotBeEmpty(t *testing.T) { - responseTime := 10 * time.Second - tickerInterval := 1 * time.Second + responseTime := 10 * time.Millisecond + tickerInterval := 1 * time.Millisecond d := NewDetector(responseTime, tickerInterval) d.AddMessage(command.GetAddr) - time.Sleep(1 * time.Second) + time.Sleep(1 * time.Millisecond) k := make(map[command.Type]time.Time) d.lock.RLock() diff --git a/pkg/vm/stack/Int.go b/pkg/vm/stack/Int.go new file mode 100644 index 000000000..ac9a4cdba --- /dev/null +++ b/pkg/vm/stack/Int.go @@ -0,0 +1,94 @@ +package stack + +import "math/big" + +// Int represents an integer on the stack +type Int struct { + *abstractItem + val *big.Int +} + +// NewInt will convert a big integer into +// a StackInteger +func NewInt(val *big.Int) (*Int, error) { + return &Int{ + abstractItem: &abstractItem{}, + val: val, + }, nil +} + +// Equal will check if two integers hold equal value +func (i *Int) Equal(s *Int) bool { + if i.val.Cmp(s.val) != 0 { + return false + } + return true +} + +// Add will add two stackIntegers together +func (i *Int) Add(s *Int) (*Int, error) { + return &Int{ + val: new(big.Int).Add(i.val, s.val), + }, nil +} + +// Sub will subtract two stackIntegers together +func (i *Int) Sub(s *Int) (*Int, error) { + return &Int{ + val: new(big.Int).Sub(i.val, s.val), + }, nil +} + +// Mul will multiply two stackIntegers together +func (i *Int) Mul(s *Int) (*Int, error) { + return &Int{ + val: new(big.Int).Mul(i.val, s.val), + }, nil +} + +// Mod will take the mod of two stackIntegers together +func (i *Int) Mod(s *Int) (*Int, error) { + return &Int{ + val: new(big.Int).Mod(i.val, s.val), + }, nil +} + +// Rsh will shift the integer b to the right by `n` bits +func (i *Int) Rsh(n *Int) (*Int, error) { + return &Int{ + val: new(big.Int).Rsh(i.val, uint(n.val.Int64())), + }, nil +} + +// Lsh will shift the integer b to the left by `n` bits +func (i *Int) Lsh(n *Int) (*Int, error) { + return &Int{ + val: new(big.Int).Lsh(i.val, uint(n.val.Int64())), + }, nil +} + +// Integer will overwrite the default implementation +// to allow go to cast this item as an integer. +func (i *Int) Integer() (*Int, error) { + return i, nil +} + +// ByteArray override the default ByteArray method +// to convert a Integer into a byte Array +func (i *Int) ByteArray() (*ByteArray, error) { + b := i.val.Bytes() + dest := reverse(b) + return NewByteArray(dest), nil +} + +//Boolean override the default Boolean method +// to convert an Integer into a Boolean StackItem +func (i *Int) Boolean() (*Boolean, error) { + boolean := (i.val.Int64() != 0) + return NewBoolean(boolean), nil +} + +//Value returns the underlying big.Int +func (i *Int) Value() *big.Int { + return i.val +} diff --git a/pkg/vm/stack/Readme.md b/pkg/vm/stack/Readme.md new file mode 100644 index 000000000..2e1b6ba78 --- /dev/null +++ b/pkg/vm/stack/Readme.md @@ -0,0 +1,24 @@ +## VM - Stack + +- How do i implement a new StackItem? + +Answer: You add it's type to the Item interface, then you implement the default return method on the abstract stack item, this should be the behaviour of the stack item, if it is not the new type. Then you embed the abstract item in the new struct and override the method. + +For example, If I wanted to add a new type called `HashMap` + +type Item interface{ + HashMap()(*HashMap, error) +} + +func (a *abstractItem) HashMap() (*HashMap, error) { + return nil, errors.New(This stack item is not a hashmap) +} + +type HashMap struct { + *abstractItem + // Variables needed for hashmap +} + +func (h *HashMap) HashMap()(*HashMap, error) { + // logic to override default behaviour +} diff --git a/pkg/vm/stack/array.go b/pkg/vm/stack/array.go new file mode 100644 index 000000000..96fe876a4 --- /dev/null +++ b/pkg/vm/stack/array.go @@ -0,0 +1,13 @@ +package stack + +// Array represents an Array of stackItems on the stack +type Array struct { + *abstractItem + val []Item +} + +// Array overrides the default implementation +// by the abstractItem, returning an Array struct +func (a *Array) Array() (*Array, error) { + return a, nil +} diff --git a/pkg/vm/stack/boolean.go b/pkg/vm/stack/boolean.go new file mode 100644 index 000000000..66e3647e5 --- /dev/null +++ b/pkg/vm/stack/boolean.go @@ -0,0 +1,26 @@ +package stack + +// Boolean represents a boolean value on the stack +type Boolean struct { + *abstractItem + val bool +} + +//NewBoolean returns a new boolean stack item +func NewBoolean(val bool) *Boolean { + return &Boolean{ + &abstractItem{}, + val, + } +} + +// Boolean overrides the default implementation +// by the abstractItem, returning a Boolean struct +func (b *Boolean) Boolean() (*Boolean, error) { + return b, nil +} + +// Value returns the underlying boolean value +func (b *Boolean) Value() bool { + return b.val +} diff --git a/pkg/vm/stack/builder.go b/pkg/vm/stack/builder.go new file mode 100644 index 000000000..e50587aff --- /dev/null +++ b/pkg/vm/stack/builder.go @@ -0,0 +1,177 @@ +package stack + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// Builder follows the builder pattern and will be used to build scripts +type Builder struct { + w *bytes.Buffer + err error +} + +// NewBuilder returns a new builder object +func NewBuilder() *Builder { + return &Builder{ + w: &bytes.Buffer{}, + err: nil, + } +} + +// Bytes returns the byte representation of the built buffer +func (br *Builder) Bytes() []byte { + return br.w.Bytes() +} + +// Emit a VM Opcode with data to the given buffer. +func (br *Builder) Emit(op Instruction, b []byte) *Builder { + if br.err != nil { + return br + } + br.err = br.w.WriteByte(byte(op)) + _, br.err = br.w.Write(b) + return br +} + +// EmitOpcode emits a single VM Opcode the given buffer. +func (br *Builder) EmitOpcode(op Instruction) *Builder { + if br.err != nil { + return br + } + br.err = br.w.WriteByte(byte(op)) + return br +} + +// EmitBool emits a bool type the given buffer. +func (br *Builder) EmitBool(ok bool) *Builder { + if br.err != nil { + return br + } + op := PUSHT + if !ok { + op = PUSHF + } + return br.EmitOpcode(op) +} + +// EmitInt emits a int type to the given buffer. +func (br *Builder) EmitInt(i int64) *Builder { + if br.err != nil { + return br + } + if i == -1 { + return br.EmitOpcode(PUSHM1) + } + if i == 0 { + return br.EmitOpcode(PUSHF) + } + if i > 0 && i < 16 { + val := Instruction(int(PUSH1) - 1 + int(i)) + return br.EmitOpcode(val) + } + + bInt := big.NewInt(i) + val := reverse(bInt.Bytes()) + return br.EmitBytes(val) +} + +// EmitString emits a string to the given buffer. +func (br *Builder) EmitString(s string) *Builder { + if br.err != nil { + return br + } + return br.EmitBytes([]byte(s)) +} + +// EmitBytes emits a byte array to the given buffer. +func (br *Builder) EmitBytes(b []byte) *Builder { + if br.err != nil { + return br + } + var ( + n = len(b) + ) + + if n <= int(PUSHBYTES75) { + return br.Emit(Instruction(n), b) + } else if n < 0x100 { + br.Emit(PUSHDATA1, []byte{byte(n)}) + } else if n < 0x10000 { + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(n)) + br.Emit(PUSHDATA2, buf) + } else { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(n)) + br.Emit(PUSHDATA4, buf) + } + _, br.err = br.w.Write(b) + return br +} + +// EmitSyscall emits the syscall API to the given buffer. +// Syscall API string cannot be 0. +func (br *Builder) EmitSyscall(api string) *Builder { + if br.err != nil { + return br + } + if len(api) == 0 { + br.err = errors.New("syscall api cannot be of length 0") + } + buf := make([]byte, len(api)+1) + buf[0] = byte(len(api)) + copy(buf[1:], []byte(api)) + return br.Emit(SYSCALL, buf) +} + +// EmitCall emits a call Opcode with label to the given buffer. +func (br *Builder) EmitCall(op Instruction, label int16) *Builder { + return br.EmitJmp(op, label) +} + +// EmitJmp emits a jump Opcode along with label to the given buffer. +func (br *Builder) EmitJmp(op Instruction, label int16) *Builder { + if !isOpcodeJmp(op) { + br.err = fmt.Errorf("opcode %d is not a jump or call type", op) + } + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(label)) + return br.Emit(op, buf) +} + +// EmitAppCall emits an appcall, if tailCall is true, tailCall opcode will be +// emitted instead. +func (br *Builder) EmitAppCall(scriptHash util.Uint160, tailCall bool) *Builder { + op := APPCALL + if tailCall { + op = TAILCALL + } + return br.Emit(op, scriptHash.Bytes()) +} + +// EmitAppCallWithOperationAndData emits an appcall with the given operation and data. +func (br *Builder) EmitAppCallWithOperationAndData(w *bytes.Buffer, scriptHash util.Uint160, operation string, data []byte) *Builder { + br.EmitBytes(data) + br.EmitString(operation) + return br.EmitAppCall(scriptHash, false) +} + +// EmitAppCallWithOperation emits an appcall with the given operation. +func (br *Builder) EmitAppCallWithOperation(scriptHash util.Uint160, operation string) *Builder { + br.EmitBool(false) + br.EmitString(operation) + return br.EmitAppCall(scriptHash, false) +} + +func isOpcodeJmp(op Instruction) bool { + if op == JMP || op == JMPIFNOT || op == JMPIF || op == CALL { + return true + } + return false +} diff --git a/pkg/vm/stack/bytearray.go b/pkg/vm/stack/bytearray.go new file mode 100644 index 000000000..7d1c3c818 --- /dev/null +++ b/pkg/vm/stack/bytearray.go @@ -0,0 +1,71 @@ +package stack + +import ( + "bytes" + "errors" + "math/big" + "strconv" +) + +// ByteArray represents a slice of bytes on the stack +type ByteArray struct { + *abstractItem + val []byte +} + +//NewByteArray returns a ByteArray stack item +// given a byte slice +func NewByteArray(val []byte) *ByteArray { + return &ByteArray{ + &abstractItem{}, + val, + } +} + +//ByteArray overrides the default abstractItem Bytes array method +func (ba *ByteArray) ByteArray() (*ByteArray, error) { + return ba, nil +} + +//Equals returns true, if two bytearrays are equal +func (ba *ByteArray) Equals(other *ByteArray) *Boolean { + // If either are nil, return false + if ba == nil || other == nil { + return NewBoolean(false) + } + return NewBoolean(bytes.Equal(ba.val, other.val)) +} + +//Integer overrides the default Integer method to convert an +// ByteArray Into an integer +func (ba *ByteArray) Integer() (*Int, error) { + dest := reverse(ba.val) + integerVal := new(big.Int).SetBytes(dest) + return NewInt(integerVal) + +} + +// Boolean will convert a byte array into a boolean stack item +func (ba *ByteArray) Boolean() (*Boolean, error) { + boolean, err := strconv.ParseBool(string(ba.val)) + if err != nil { + return nil, errors.New("cannot convert byte array to a boolean") + } + return NewBoolean(boolean), nil +} + +// XXX: move this into a pkg/util/slice folder +// Go mod not working +func reverse(b []byte) []byte { + if len(b) < 2 { + return b + } + + dest := make([]byte, len(b)) + + for i, j := 0, len(b)-1; i < j+1; i, j = i+1, j-1 { + dest[i], dest[j] = b[j], b[i] + } + + return dest +} diff --git a/pkg/vm/stack/context.go b/pkg/vm/stack/context.go new file mode 100644 index 000000000..d381d74cb --- /dev/null +++ b/pkg/vm/stack/context.go @@ -0,0 +1,152 @@ +package stack + +import ( + "encoding/binary" + "errors" +) + +// Context represent the current execution context of the VM. +// context will be treated as stack item and placed onto the invocation stack +type Context struct { + *abstractItem + + // Instruction pointer. + ip int + + // The raw program script. + prog []byte + + // Breakpoints + breakPoints []int + + // Evaluation Stack + Estack RandomAccess + + // Alternative Stack + Astack RandomAccess +} + +// NewContext return a new Context object. +func NewContext(b []byte) *Context { + return &Context{ + abstractItem: &abstractItem{}, + ip: -1, + prog: b, + breakPoints: []int{}, + } +} + +// Context overrides the default implementation +// to return a context item +func (c *Context) Context() (*Context, error) { + return c, nil +} + +// Next return the next instruction to execute. +func (c *Context) Next() (Instruction, error) { + c.ip++ + if c.ip >= len(c.prog) { + return RET, errors.New("program pointer is more than the length of program. Returning RET OPCODE") + } + return Instruction(c.prog[c.ip]), nil +} + +// IP returns the absolute instruction without taking 0 into account. +// If that program starts the ip = 0 but IP() will return 1, cause its +// the first instruction. +func (c *Context) IP() int { + return c.ip + 1 +} + +// LenInstr returns the number of instructions loaded. +func (c *Context) LenInstr() int { + return len(c.prog) +} + +// CurrInstr returns the current instruction and opcode. +func (c *Context) CurrInstr() (int, Instruction) { + if c.ip < 0 { + return c.ip, Instruction(0x00) + } + 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, + } +} + +// Program returns the loaded program. +func (c *Context) Program() []byte { + return c.prog +} + +func (c *Context) atBreakPoint() bool { + for _, n := range c.breakPoints { + if n == c.ip { + return true + } + } + return false +} + +func (c *Context) String() string { + return "execution context" +} + +// ReadUint32 reads a uint32 from the script +func (c *Context) ReadUint32() uint32 { + start, end := c.IP(), c.IP()+4 + if end > len(c.prog) { + return 0 + } + val := binary.LittleEndian.Uint32(c.prog[start:end]) + c.ip += 4 + return val +} + +// ReadUint16 reads a uint16 from the script +func (c *Context) ReadUint16() uint16 { + start, end := c.IP(), c.IP()+2 + if end > len(c.prog) { + return 0 + } + val := binary.LittleEndian.Uint16(c.prog[start:end]) + c.ip += 2 + return val +} + +// ReadByte reads one byte from the script +func (c *Context) ReadByte() (byte, error) { + byt, err := c.ReadBytes(1) + if err != nil { + return 0, err + } + + return byt[0], nil +} + +// ReadBytes will read n bytes from the context +func (c *Context) ReadBytes(n int) ([]byte, error) { + start, end := c.IP(), c.IP()+n + if end > len(c.prog) { + return nil, errors.New("Too many bytes to read, pointer goes past end of program") + } + + out := make([]byte, n) + copy(out, c.prog[start:end]) + c.ip += n + return out, nil +} + +func (c *Context) readVarBytes() ([]byte, error) { + n, err := c.ReadByte() + if err != nil { + return nil, err + } + return c.ReadBytes(int(n)) +} diff --git a/pkg/vm/stack/instruction.go b/pkg/vm/stack/instruction.go new file mode 100644 index 000000000..1317a1e3e --- /dev/null +++ b/pkg/vm/stack/instruction.go @@ -0,0 +1,133 @@ +package stack + +// Instruction represents a operation code in the neovm +type Instruction byte + +// Viable list of supported instruction constants. +const ( + // Constants + PUSH0 Instruction = 0x00 + PUSHF Instruction = PUSH0 + PUSHBYTES1 Instruction = 0x01 + PUSHBYTES75 Instruction = 0x4B + PUSHDATA1 Instruction = 0x4C + PUSHDATA2 Instruction = 0x4D + PUSHDATA4 Instruction = 0x4E + PUSHM1 Instruction = 0x4F + PUSH1 Instruction = 0x51 + PUSHT Instruction = PUSH1 + PUSH2 Instruction = 0x52 + PUSH3 Instruction = 0x53 + PUSH4 Instruction = 0x54 + PUSH5 Instruction = 0x55 + PUSH6 Instruction = 0x56 + PUSH7 Instruction = 0x57 + PUSH8 Instruction = 0x58 + PUSH9 Instruction = 0x59 + PUSH10 Instruction = 0x5A + PUSH11 Instruction = 0x5B + PUSH12 Instruction = 0x5C + PUSH13 Instruction = 0x5D + PUSH14 Instruction = 0x5E + PUSH15 Instruction = 0x5F + PUSH16 Instruction = 0x60 + + // Flow control + NOP Instruction = 0x61 + JMP Instruction = 0x62 + JMPIF Instruction = 0x63 + JMPIFNOT Instruction = 0x64 + CALL Instruction = 0x65 + RET Instruction = 0x66 + APPCALL Instruction = 0x67 + SYSCALL Instruction = 0x68 + TAILCALL Instruction = 0x69 + + // Stack + DUPFROMALTSTACK Instruction = 0x6A + TOALTSTACK Instruction = 0x6B + FROMALTSTACK Instruction = 0x6C + XDROP Instruction = 0x6D + XSWAP Instruction = 0x72 + XTUCK Instruction = 0x73 + DEPTH Instruction = 0x74 + DROP Instruction = 0x75 + DUP Instruction = 0x76 + NIP Instruction = 0x77 + OVER Instruction = 0x78 + PICK Instruction = 0x79 + ROLL Instruction = 0x7A + ROT Instruction = 0x7B + SWAP Instruction = 0x7C + TUCK Instruction = 0x7D + + // Splice + CAT Instruction = 0x7E + SUBSTR Instruction = 0x7F + LEFT Instruction = 0x80 + RIGHT Instruction = 0x81 + SIZE Instruction = 0x82 + + // Bitwise logic + INVERT Instruction = 0x83 + AND Instruction = 0x84 + OR Instruction = 0x85 + XOR Instruction = 0x86 + EQUAL Instruction = 0x87 + + // Arithmetic + INC Instruction = 0x8B + DEC Instruction = 0x8C + SIGN Instruction = 0x8D + NEGATE Instruction = 0x8F + ABS Instruction = 0x90 + NOT Instruction = 0x91 + NZ Instruction = 0x92 + ADD Instruction = 0x93 + SUB Instruction = 0x94 + MUL Instruction = 0x95 + DIV Instruction = 0x96 + MOD Instruction = 0x97 + SHL Instruction = 0x98 + SHR Instruction = 0x99 + BOOLAND Instruction = 0x9A + BOOLOR Instruction = 0x9B + NUMEQUAL Instruction = 0x9C + NUMNOTEQUAL Instruction = 0x9E + LT Instruction = 0x9F + GT Instruction = 0xA0 + LTE Instruction = 0xA1 + GTE Instruction = 0xA2 + MIN Instruction = 0xA3 + MAX Instruction = 0xA4 + WITHIN Instruction = 0xA5 + + // Crypto + SHA1 Instruction = 0xA7 + SHA256 Instruction = 0xA8 + HASH160 Instruction = 0xA9 + HASH256 Instruction = 0xAA + CHECKSIG Instruction = 0xAC + CHECKMULTISIG Instruction = 0xAE + + // Array + ARRAYSIZE Instruction = 0xC0 + PACK Instruction = 0xC1 + UNPACK Instruction = 0xC2 + PICKITEM Instruction = 0xC3 + SETITEM Instruction = 0xC4 + NEWARRAY Instruction = 0xC5 + NEWSTRUCT Instruction = 0xC6 + APPEND Instruction = 0xC8 + REVERSE Instruction = 0xC9 + REMOVE Instruction = 0xCA + + // Exceptions + THROW Instruction = 0xF0 + THROWIFNOT Instruction = 0xF1 +) + +// Value returns the byte-value of the opcode. +func (i Instruction) Value() byte { + return byte(i) +} diff --git a/pkg/vm/stack/int_test.go b/pkg/vm/stack/int_test.go new file mode 100644 index 000000000..25d360183 --- /dev/null +++ b/pkg/vm/stack/int_test.go @@ -0,0 +1,72 @@ +package stack + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAdd(t *testing.T) { + a := testMakeStackInt(t, 10) + b := testMakeStackInt(t, 20) + expected := testMakeStackInt(t, 30) + c, err := a.Add(b) + assert.Nil(t, err) + assert.Equal(t, true, expected.Equal(c)) +} +func TestSub(t *testing.T) { + a := testMakeStackInt(t, 30) + b := testMakeStackInt(t, 200) + expected := testMakeStackInt(t, 170) + c, err := b.Sub(a) + assert.Nil(t, err) + assert.Equal(t, true, expected.Equal(c)) +} +func TestMul(t *testing.T) { + a := testMakeStackInt(t, 10) + b := testMakeStackInt(t, 20) + expected := testMakeStackInt(t, 200) + c, err := a.Mul(b) + assert.Nil(t, err) + assert.Equal(t, true, expected.Equal(c)) +} +func TestMod(t *testing.T) { + a := testMakeStackInt(t, 10) + b := testMakeStackInt(t, 20) + expected := testMakeStackInt(t, 10) + c, err := a.Mod(b) + assert.Nil(t, err) + assert.Equal(t, true, expected.Equal(c)) +} +func TestLsh(t *testing.T) { + a := testMakeStackInt(t, 23) + b := testMakeStackInt(t, 8) + expected := testMakeStackInt(t, 5888) + c, err := a.Lsh(b) + assert.Nil(t, err) + assert.Equal(t, true, expected.Equal(c)) +} + +func TestRsh(t *testing.T) { + a := testMakeStackInt(t, 128) + b := testMakeStackInt(t, 3) + expected := testMakeStackInt(t, 16) + c, err := a.Rsh(b) + assert.Nil(t, err) + assert.Equal(t, true, expected.Equal(c)) +} + +func TestByteArrConversion(t *testing.T) { + + var num int64 = 100000 + + a := testMakeStackInt(t, num) + ba, err := a.ByteArray() + assert.Nil(t, err) + + have, err := ba.Integer() + assert.Nil(t, err) + + assert.Equal(t, num, have.val.Int64()) + +} diff --git a/pkg/vm/stack/invocationstack.go b/pkg/vm/stack/invocationstack.go new file mode 100644 index 000000000..49f058f62 --- /dev/null +++ b/pkg/vm/stack/invocationstack.go @@ -0,0 +1,61 @@ +package stack + +import "errors" + +// Invocation embeds a Random Access stack +// Providing helper methods for the context object +type Invocation struct{ RandomAccess } + +//NewInvocation will return a new +// Invocation stack +func NewInvocation() *Invocation { + return &Invocation{ + RandomAccess{ + vals: make([]Item, 0, StackAverageSize), + }, + } +} + +func (i *Invocation) peekContext(n uint16) (*Context, error) { + item, err := i.Peek(n) + if err != nil { + return nil, err + } + return item.Context() +} + +// CurrentContext returns the current context on the invocation stack +func (i *Invocation) CurrentContext() (*Context, error) { + return i.peekContext(0) +} + +// PopCurrentContext Pops a context item from the top of the stack +func (i *Invocation) PopCurrentContext() (*Context, error) { + item, err := i.Pop() + if err != nil { + return nil, err + } + ctx, err := item.Context() + if err != nil { + return nil, err + } + return ctx, err +} + +// CallingContext will return the cntext item +// that will be called next. +func (i *Invocation) CallingContext() (*Context, error) { + if i.Len() < 1 { + return nil, errors.New("Length of invocation stack is < 1, no calling context") + } + return i.peekContext(1) +} + +// EntryContext will return the context item that +// started the program +func (i *Invocation) EntryContext() (*Context, error) { + + // firstItemIndex refers to the first item that was popped on the stack + firstItemIndex := uint16(i.Len() - 1) // N.B. if this overflows because len is zero, then an error will be returned + return i.peekContext(firstItemIndex) +} diff --git a/pkg/vm/stack/stack.go b/pkg/vm/stack/stack.go new file mode 100644 index 000000000..8d1ac5b78 --- /dev/null +++ b/pkg/vm/stack/stack.go @@ -0,0 +1,150 @@ +package stack + +import ( + "errors" + "fmt" +) + +const ( + // StackAverageSize is used to set the capacity of the stack + // setting this number too low, will cause extra allocations + StackAverageSize = 20 +) + +// RandomAccess represents a Random Access Stack +type RandomAccess struct { + vals []Item +} + +// New will return a new random access stack +func New() *RandomAccess { + return &RandomAccess{ + vals: make([]Item, 0, StackAverageSize), + } +} + +// Items will return all items in the stack +func (ras *RandomAccess) items() []Item { + return ras.vals +} + +//Len will return the length of the stack +func (ras *RandomAccess) Len() int { + if ras.vals == nil { + return -1 + } + return len(ras.vals) +} + +// Clear will remove all items in the stack +func (ras *RandomAccess) Clear() { + ras.vals = make([]Item, 0, StackAverageSize) +} + +// Pop will remove the last stack item that was added +func (ras *RandomAccess) Pop() (Item, error) { + if len(ras.vals) == 0 { + return nil, errors.New("There are no items on the stack to pop") + } + if ras.vals == nil { + return nil, errors.New("Cannot pop from a nil stack") + } + + l := len(ras.vals) + item := ras.vals[l-1] + ras.vals = ras.vals[:l-1] + + return item, nil +} + +// Push will put a stack item onto the top of the stack +func (ras *RandomAccess) Push(item Item) *RandomAccess { + if ras.vals == nil { + ras.vals = make([]Item, 0, StackAverageSize) + } + + ras.vals = append(ras.vals, item) + + return ras +} + +// Insert will push a stackItem onto the stack at position `n` +// Note; index 0 is the top of the stack, which is the end of slice +func (ras *RandomAccess) Insert(n uint16, item Item) (*RandomAccess, error) { + + if n == 0 { + return ras.Push(item), nil + } + + if ras.vals == nil { + ras.vals = make([]Item, 0, StackAverageSize) + } + + // Check that we are not inserting out of the bounds + stackSize := uint16(len(ras.vals)) + if n > stackSize-1 { + return nil, fmt.Errorf("Tried to insert at index %d when length of stack is %d", n, len(ras.vals)) + } + + index := stackSize - n + + ras.vals = append(ras.vals, item) + copy(ras.vals[index:], ras.vals[index-1:]) + ras.vals[index] = item + + return ras, nil +} + +// Peek will check an element at a given index +// Note: 0 is the top of the stack, which is the end of the slice +func (ras *RandomAccess) Peek(n uint16) (Item, error) { + + stackSize := uint16(len(ras.vals)) + + if n == 0 { + index := stackSize - 1 + return ras.vals[index], nil + } + + if ras.Len() < 1 { + return nil, fmt.Errorf("cannot peak at a stack with no item, length of stack is %d", ras.Len()) + } + + // Check that we are not peeking out of the bounds + if n > stackSize-1 { + return nil, fmt.Errorf("Tried to peek at index %d when length of stack is %d", n, len(ras.vals)) + } + index := stackSize - n - 1 + + return ras.vals[index], nil +} + +// CopyTo will copy all of the stack items from `ras` into the stack that is passed as an argument +// XXX: once maxstacksize is implemented, we will return error if size goes over +// There will also be additional checks needed once stack isolation is added +func (ras *RandomAccess) CopyTo(stack *RandomAccess) error { + stack.vals = append(stack.vals, ras.vals...) + return nil +} + +// Convenience Functions + +// PopInt will remove the last stack item that was added +// And cast it to an integer +func (ras *RandomAccess) PopInt() (*Int, error) { + item, err := ras.Pop() + if err != nil { + return nil, err + } + return item.Integer() +} + +// PopByteArray will remove the last stack item that was added +// And cast it to an ByteArray +func (ras *RandomAccess) PopByteArray() (*ByteArray, error) { + item, err := ras.Pop() + if err != nil { + return nil, err + } + return item.ByteArray() +} diff --git a/pkg/vm/stack/stack_test.go b/pkg/vm/stack/stack_test.go new file mode 100644 index 000000000..246e983f4 --- /dev/null +++ b/pkg/vm/stack/stack_test.go @@ -0,0 +1,161 @@ +package stack + +import ( + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStackPushPop(t *testing.T) { + // Create two stack Integers + a, err := NewInt(big.NewInt(10)) + if err != nil { + t.Fail() + } + b, err := NewInt(big.NewInt(20)) + if err != nil { + t.Fail() + } + + // Create a new stack + testStack := New() + + // Push to stack + testStack.Push(a).Push(b) + + // There should only be two values on the stack + assert.Equal(t, 2, testStack.Len()) + + // Pop first element and it should be equal to b + stackElement, err := testStack.Pop() + if err != nil { + t.Fail() + } + item, err := stackElement.Integer() + if err != nil { + t.Fail() + } + assert.Equal(t, true, item.Equal(b)) + + // Pop second element and it should be equal to a + stackElement, err = testStack.Pop() + if err != nil { + t.Fail() + } + item, err = stackElement.Integer() + if err != nil { + t.Fail() + } + assert.Equal(t, true, item.Equal(a)) + + // We should get an error as there are nomore items left to pop + stackElement, err = testStack.Pop() + assert.NotNil(t, err) + +} + +// For this test to pass, we should get an error when popping from a nil stack +// and we should initialise and push an element if pushing to an empty stack +func TestPushPopNil(t *testing.T) { + + // stack is nil when initialised without New constructor + testStack := RandomAccess{} + + // Popping from nil stack + // - should give an error + // - element returned should be nil + stackElement, err := testStack.Pop() + assert.NotNil(t, err) + assert.Nil(t, stackElement) + + // stack should still be nil after failing to pop + assert.Nil(t, testStack.vals) + + // create a random test stack item + a, err := NewInt(big.NewInt(2)) + assert.Nil(t, err) + + // push random item to stack + testStack.Push(a) + + // push should initialise the stack and put one element on the stack + assert.Equal(t, 1, testStack.Len()) +} + +// Test passes if we can peek and modify an item +//without modifying the value on the stack +func TestStackPeekMutability(t *testing.T) { + + testStack := New() + + a, err := NewInt(big.NewInt(2)) + assert.Nil(t, err) + b, err := NewInt(big.NewInt(3)) + assert.Nil(t, err) + + testStack.Push(a).Push(b) + + peekedItem := testPeakInteger(t, testStack, 0) + assert.Equal(t, true, peekedItem.Equal(b)) + + // Check that by modifying the peeked value, + // we did not modify the item on the stack + peekedItem = a + peekedItem.val = big.NewInt(0) + + // Pop item from stack and check it is still the same + poppedItem := testPopInteger(t, testStack) + assert.Equal(t, true, poppedItem.Equal(b)) +} +func TestStackPeek(t *testing.T) { + + testStack := New() + + values := []int64{23, 45, 67, 89, 12, 344} + for _, val := range values { + a := testMakeStackInt(t, val) + testStack.Push(a) + } + + // i starts at 0, j starts at len(values)-1 + for i, j := 0, len(values)-1; j >= 0; i, j = i+1, j-1 { + + peekedItem := testPeakInteger(t, testStack, uint16(i)) + a := testMakeStackInt(t, values[j]) + + fmt.Printf("%#v\n", peekedItem.val.Int64()) + + assert.Equal(t, true, a.Equal(peekedItem)) + + } + +} + +func TestStackInsert(t *testing.T) { + + testStack := New() + + a := testMakeStackInt(t, 2) + b := testMakeStackInt(t, 4) + c := testMakeStackInt(t, 6) + + // insert on an empty stack should put element on top + _, err := testStack.Insert(0, a) + assert.Equal(t, err, nil) + _, err = testStack.Insert(0, b) + assert.Equal(t, err, nil) + _, err = testStack.Insert(1, c) + assert.Equal(t, err, nil) + + // Order should be [a,c,b] + pop1 := testPopInteger(t, testStack) + pop2 := testPopInteger(t, testStack) + pop3 := testPopInteger(t, testStack) + + assert.Equal(t, true, pop1.Equal(b)) + assert.Equal(t, true, pop2.Equal(c)) + assert.Equal(t, true, pop3.Equal(a)) + +} diff --git a/pkg/vm/stack/stackitem.go b/pkg/vm/stack/stackitem.go new file mode 100644 index 000000000..beed13363 --- /dev/null +++ b/pkg/vm/stack/stackitem.go @@ -0,0 +1,49 @@ +package stack + +import ( + "errors" +) + +//Item is an interface which represents object that can be placed on the stack +type Item interface { + Integer() (*Int, error) + Boolean() (*Boolean, error) + ByteArray() (*ByteArray, error) + Array() (*Array, error) + Context() (*Context, error) +} + +// Represents an `abstract` stack item +// which will hold default values for stack items +// this is intended to be embedded into types that you will use on the stack +type abstractItem struct{} + +// Integer is the default implementation for a stackItem +// Implements Item interface +func (a *abstractItem) Integer() (*Int, error) { + return nil, errors.New("This stack item is not an Integer") +} + +// Boolean is the default implementation for a stackItem +// Implements Item interface +func (a *abstractItem) Boolean() (*Boolean, error) { + return nil, errors.New("This stack item is not a Boolean") +} + +// ByteArray is the default implementation for a stackItem +// Implements Item interface +func (a *abstractItem) ByteArray() (*ByteArray, error) { + return nil, errors.New("This stack item is not a byte array") +} + +// Array is the default implementation for a stackItem +// Implements Item interface +func (a *abstractItem) Array() (*Array, error) { + return nil, errors.New("This stack item is not an array") +} + +// Context is the default implementation for a stackItem +// Implements Item interface +func (a *abstractItem) Context() (*Context, error) { + return nil, errors.New("This stack item is not of type context") +} diff --git a/pkg/vm/stack/stackitem_test.go b/pkg/vm/stack/stackitem_test.go new file mode 100644 index 000000000..741d6f530 --- /dev/null +++ b/pkg/vm/stack/stackitem_test.go @@ -0,0 +1,68 @@ +package stack + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +// A simple test to ensure that by embedding the abstract interface +// we immediately become a stack item, with the default values set to nil +func TestInterfaceEmbedding(t *testing.T) { + + // Create an anonymous struct that embeds the abstractItem + a := struct { + *abstractItem + }{ + &abstractItem{}, + } + + // Since interface checking can be done at compile time. + // If he abstractItem did not implement all methods of our interface `Item` + // Then any struct which embeds it, will also not implement the Item interface. + // This test would then give errors, at compile time. + var Items []Item + Items = append(Items, a) + + // Default methods should give errors + // Here we just need to test against one of the methods in the interface + for _, element := range Items { + x, err := element.Integer() + assert.Nil(t, x) + assert.NotNil(t, err, nil) + } + +} + +// TestIntCasting is a simple test to test that the Integer method is overwritten +// from the abstractItem +func TestIntMethodOverride(t *testing.T) { + + testValues := []int64{0, 10, 200, 30, 90} + var Items []Item + + // Convert a range of int64s into Stack Integers + // Adding them into an array of StackItems + for _, num := range testValues { + stackInteger, err := NewInt(big.NewInt(num)) + if err != nil { + t.Fail() + } + Items = append(Items, stackInteger) + } + + // For each item, call the Integer method on the interface + // Which should return an integer and no error + // as the stack integer struct overrides that method + for i, element := range Items { + k, err := element.Integer() + if err != nil { + t.Fail() + } + if k.val.Cmp(big.NewInt(testValues[i])) != 0 { + t.Fail() + } + } + +} diff --git a/pkg/vm/stack/test_helper.go b/pkg/vm/stack/test_helper.go new file mode 100644 index 000000000..15c6f87de --- /dev/null +++ b/pkg/vm/stack/test_helper.go @@ -0,0 +1,44 @@ +package stack + +import ( + "bytes" + "encoding/binary" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +// helper functions +func testPeakInteger(t *testing.T, tStack *RandomAccess, n uint16) *Int { + stackElement, err := tStack.Peek(n) + assert.Nil(t, err) + item, err := stackElement.Integer() + if err != nil { + t.Fail() + } + return item +} + +func testPopInteger(t *testing.T, tStack *RandomAccess) *Int { + stackElement, err := tStack.Pop() + assert.Nil(t, err) + item, err := stackElement.Integer() + if err != nil { + t.Fail() + } + return item +} + +func testMakeStackInt(t *testing.T, num int64) *Int { + a, err := NewInt(big.NewInt(num)) + assert.Nil(t, err) + return a +} + +func testReadInt64(data []byte) int64 { + var ret int64 + buf := bytes.NewBuffer(data) + binary.Read(buf, binary.LittleEndian, &ret) + return ret +} diff --git a/pkg/vm/state.go b/pkg/vm/state.go new file mode 100644 index 000000000..e6760c7c9 --- /dev/null +++ b/pkg/vm/state.go @@ -0,0 +1,20 @@ +package vm + +//Vmstate represents all possible states that the neo-vm can be in +type Vmstate byte + +// List of possible vm states +const ( + // NONE is the running state of the vm + // NONE signifies that the vm is ready to process an opcode + NONE = 0 + // HALT is a stopped state of the vm + // where the stop was signalled by the program completion + HALT = 1 << 0 + // FAULT is a stopped state of the vm + // where the stop was signalled by an error in the program + FAULT = 1 << 1 + // BREAK is a suspended state for the VM + // were the break was signalled by a breakpoint + BREAK = 1 << 2 +) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go new file mode 100644 index 000000000..e207a7ed6 --- /dev/null +++ b/pkg/vm/vm.go @@ -0,0 +1,72 @@ +package vm + +import ( + "fmt" + + "github.com/CityOfZion/neo-go/pkg/vm/stack" +) + +// VM represents an instance of a Neo Virtual Machine +type VM struct { + // ResultStack contains the results of + // the last evaluation stack before the program terminated + ResultStack stack.RandomAccess + // InvocationStack contains all of the contexts + // loaded into the vm + InvocationStack stack.Invocation + state Vmstate +} + +// NewVM will: +// Set the state of the VM to NONE +// instantiate a script as a new context +// Push the Context to the Invocation stack +func NewVM(script []byte) *VM { + ctx := stack.NewContext(script) + v := &VM{ + state: NONE, + } + v.InvocationStack.Push(ctx) + return v +} + +// Run loops over the current context by continuously stepping. +// Run breaks; once step returns an error or any state that is not NONE +func (v *VM) Run() (Vmstate, error) { + for { + state, err := v.step() + if err != nil || state != NONE { + return state, err + } + } +} + +// step will read `one` opcode from the script in the current context +// Then excute that opcode +func (v *VM) step() (Vmstate, error) { + // Get Current Context + ctx, err := v.InvocationStack.CurrentContext() + if err != nil { + return FAULT, err + } + // Read Opcode from context + op, _ := ctx.Next() // The only error that can occur from this, is if the pointer goes over the pointer + // In the NEO-VM specs, this is ignored and we return the RET opcode + // Execute OpCode + state, err := v.executeOp(stack.Instruction(op), ctx) + if err != nil { + return FAULT, err + } + return state, nil +} + +// ExecuteOp will execute one opcode on a given context. +// If the opcode is not registered, then an unknown opcode error will be returned +func (v *VM) executeOp(op stack.Instruction, ctx *stack.Context) (Vmstate, error) { + //Find function which handles that specific opcode + handleOp, ok := opFunc[op] + if !ok { + return FAULT, fmt.Errorf("unknown opcode entered %v", op) + } + return handleOp(op, ctx, &v.InvocationStack, &v.ResultStack) +} diff --git a/pkg/vm/vm_ops.go b/pkg/vm/vm_ops.go new file mode 100644 index 000000000..39b796389 --- /dev/null +++ b/pkg/vm/vm_ops.go @@ -0,0 +1,21 @@ +package vm + +import "github.com/CityOfZion/neo-go/pkg/vm/stack" + +type stackInfo func(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) + +var opFunc = map[stack.Instruction]stackInfo{ + stack.ADD: Add, + stack.SUB: Sub, + stack.PUSHBYTES1: PushNBytes, + stack.PUSHBYTES75: PushNBytes, + stack.RET: RET, + stack.EQUAL: EQUAL, + stack.THROWIFNOT: THROWIFNOT, +} + +func init() { + for i := int(stack.PUSHBYTES1); i <= int(stack.PUSHBYTES75); i++ { + opFunc[stack.Instruction(i)] = PushNBytes + } +} diff --git a/pkg/vm/vm_ops_bitwise.go b/pkg/vm/vm_ops_bitwise.go new file mode 100644 index 000000000..350543fa2 --- /dev/null +++ b/pkg/vm/vm_ops_bitwise.go @@ -0,0 +1,17 @@ +package vm + +import "github.com/CityOfZion/neo-go/pkg/vm/stack" + +// Bitwise logic + +// EQUAL pushes true to the stack +// If the two top items on the stack are equal +func EQUAL(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + + itemA, itemB, err := popTwoByteArrays(ctx) + if err != nil { + return FAULT, err + } + ctx.Estack.Push(itemA.Equals(itemB)) + return NONE, nil +} diff --git a/pkg/vm/vm_ops_exceptions.go b/pkg/vm/vm_ops_exceptions.go new file mode 100644 index 000000000..dd09cfb60 --- /dev/null +++ b/pkg/vm/vm_ops_exceptions.go @@ -0,0 +1,33 @@ +package vm + +import ( + "errors" + + "github.com/CityOfZion/neo-go/pkg/vm/stack" +) + +// vm exceptions + +// THROWIFNOT faults if the item on the top of the stack +// does not evaluate to true +// For specific logic on how a number of bytearray is evaluated can be seen +// from the boolean conversion methods on the stack items +func THROWIFNOT(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + + // Pop item from top of stack + item, err := ctx.Estack.Pop() + if err != nil { + return FAULT, err + } + // Convert to a boolean + ok, err := item.Boolean() + if err != nil { + return FAULT, err + } + + // If false, throw + if !ok.Value() { + return FAULT, errors.New("item on top of stack evaluates to false") + } + return NONE, nil +} diff --git a/pkg/vm/vm_ops_flow.go b/pkg/vm/vm_ops_flow.go new file mode 100644 index 000000000..67ca4f825 --- /dev/null +++ b/pkg/vm/vm_ops_flow.go @@ -0,0 +1,27 @@ +package vm + +import ( + "github.com/CityOfZion/neo-go/pkg/vm/stack" +) + +// Flow control + +// RET Returns from the current context +// Returns HALT if there are nomore context's to run +func RET(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + + // Pop current context from the Inovation stack + ctx, err := istack.PopCurrentContext() + if err != nil { + return FAULT, err + } + // If this was the last context, then we copy over the evaluation stack to the resultstack + // As the program is about to terminate, once we remove the context + if istack.Len() == 0 { + + err = ctx.Estack.CopyTo(rstack) + return HALT, err + } + + return NONE, nil +} diff --git a/pkg/vm/vm_ops_maths.go b/pkg/vm/vm_ops_maths.go new file mode 100644 index 000000000..a15596a30 --- /dev/null +++ b/pkg/vm/vm_ops_maths.go @@ -0,0 +1,70 @@ +package vm + +import ( + "github.com/CityOfZion/neo-go/pkg/vm/stack" +) + +// Add adds two stack Items together. +// Returns an error if either items cannot be casted to an integer +// or if integers cannot be added together +func Add(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + + operandA, operandB, err := popTwoIntegers(ctx) + if err != nil { + return FAULT, err + } + res, err := operandA.Add(operandB) + if err != nil { + return FAULT, err + } + + ctx.Estack.Push(res) + + return NONE, nil +} + +// Sub subtracts two stack Items. +// Returns an error if either items cannot be casted to an integer +// or if integers cannot be subtracted together +func Sub(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + + operandA, operandB, err := popTwoIntegers(ctx) + if err != nil { + return FAULT, err + } + res, err := operandB.Sub(operandA) + if err != nil { + return HALT, err + } + + ctx.Estack.Push(res) + + return NONE, nil +} + +func popTwoIntegers(ctx *stack.Context) (*stack.Int, *stack.Int, error) { + operandA, err := ctx.Estack.PopInt() + if err != nil { + return nil, nil, err + } + operandB, err := ctx.Estack.PopInt() + if err != nil { + return nil, nil, err + } + + return operandA, operandB, nil +} + +func popTwoByteArrays(ctx *stack.Context) (*stack.ByteArray, *stack.ByteArray, error) { + // Pop first stack item and cast as byte array + ba1, err := ctx.Estack.PopByteArray() + if err != nil { + return nil, nil, err + } + // Pop second stack item and cast as byte array + ba2, err := ctx.Estack.PopByteArray() + if err != nil { + return nil, nil, err + } + return ba1, ba2, nil +} diff --git a/pkg/vm/vm_ops_maths_test.go b/pkg/vm/vm_ops_maths_test.go new file mode 100644 index 000000000..4964e6923 --- /dev/null +++ b/pkg/vm/vm_ops_maths_test.go @@ -0,0 +1,69 @@ +package vm + +import ( + "math/big" + "testing" + + "github.com/CityOfZion/neo-go/pkg/vm/stack" + "github.com/stretchr/testify/assert" +) + +func TestAddOp(t *testing.T) { + + v := VM{} + + a, err := stack.NewInt(big.NewInt(20)) + if err != nil { + t.Fail() + } + b, err := stack.NewInt(big.NewInt(23)) + if err != nil { + t.Fail() + } + + ctx := stack.NewContext([]byte{}) + ctx.Estack.Push(a).Push(b) + + v.executeOp(stack.ADD, ctx) + + // Stack should have one item + assert.Equal(t, 1, ctx.Estack.Len()) + + item, err := ctx.Estack.PopInt() + if err != nil { + t.Fail() + } + + assert.Equal(t, int64(43), item.Value().Int64()) + +} + +func TestSubOp(t *testing.T) { + + v := VM{} + + a, err := stack.NewInt(big.NewInt(30)) + if err != nil { + t.Fail() + } + b, err := stack.NewInt(big.NewInt(40)) + if err != nil { + t.Fail() + } + + ctx := stack.NewContext([]byte{}) + ctx.Estack.Push(a).Push(b) + + v.executeOp(stack.SUB, ctx) + + // Stack should have one item + assert.Equal(t, 1, ctx.Estack.Len()) + + item, err := ctx.Estack.PopInt() + if err != nil { + t.Fail() + } + + assert.Equal(t, int64(-10), item.Value().Int64()) + +} diff --git a/pkg/vm/vm_ops_stackmani.go b/pkg/vm/vm_ops_stackmani.go new file mode 100644 index 000000000..f5e2ddc24 --- /dev/null +++ b/pkg/vm/vm_ops_stackmani.go @@ -0,0 +1,19 @@ +package vm + +import ( + "github.com/CityOfZion/neo-go/pkg/vm/stack" +) + +// Stack Manipulation Opcodes + +// PushNBytes will Read N Bytes from the script and push it onto the stack +func PushNBytes(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + + val, err := ctx.ReadBytes(int(op)) + if err != nil { + return FAULT, err + } + ba := stack.NewByteArray(val) + ctx.Estack.Push(ba) + return NONE, nil +} diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go new file mode 100644 index 000000000..adb8b5db3 --- /dev/null +++ b/pkg/vm/vm_test.go @@ -0,0 +1,119 @@ +package vm + +import ( + "fmt" + "testing" + + "github.com/CityOfZion/neo-go/pkg/vm/stack" + "github.com/stretchr/testify/assert" +) + +func TestPushAdd(t *testing.T) { + builder := stack.NewBuilder() + + // PUSH TWO NUMBER + // ADD THEM TOGETHER + builder.EmitInt(20).EmitInt(34).EmitOpcode(stack.ADD) + + // Pass program to VM + vm := NewVM(builder.Bytes()) + + // Execute first OPCODE + // Should be PUSH(20) + state, err := vm.step() + assert.Equal(t, NONE, int(state)) + assert.Nil(t, err) + + // We should have the number 20 on stack + ok := peekTopEStackIsValue(t, vm, 20) + assert.True(t, ok) + + // Excute second OPCODE + // Should be PUSH(34) + state, err = vm.step() + assert.Equal(t, NONE, int(state)) + assert.Nil(t, err) + + // We should have the number 34 at the top of the stack + ok = peekTopEStackIsValue(t, vm, 34) + assert.True(t, ok) + + // Excute third OPCODE + // Should Add both values on the stack + state, err = vm.step() + assert.Equal(t, NONE, int(state)) + assert.Nil(t, err) + + // We should now have one value on the stack + //It should be equal to 20+34 = 54 + ok = EstackLen(t, vm, 1) + assert.True(t, ok) + ok = peekTopEStackIsValue(t, vm, 54) + assert.True(t, ok) + + // If we try to step again, we should get a nil error and HALT + // because we have gone over the instruction pointer + // error is nil because when there are nomore instructions, the vm + // will add a RET opcode and return + state, err = vm.step() + assert.Equal(t, HALT, int(state)) + assert.Nil(t, err) + +} + +func TestSimpleRun(t *testing.T) { + + // Program pushes 20 and 34 to the stack + // Adds them together + // pushes 54 to the stack + // Checks if result of addition and 54 are equal + // Faults if not + + // Push(20) + // Push(34) + // Add + // Push(54) + // Equal + //THROWIFNOT + builder := stack.NewBuilder() + builder.EmitInt(20).EmitInt(34).EmitOpcode(stack.ADD) + builder.EmitInt(54).EmitOpcode(stack.EQUAL).EmitOpcode(stack.THROWIFNOT) + + // Pass program to VM + vm := NewVM(builder.Bytes()) + + // Runs vm with program + _, err := vm.Run() + assert.Nil(t, err) + + // ResultStack should be nil + assert.Equal(t, -1, vm.ResultStack.Len()) + +} + +// returns true if the value at the top of the evaluation stack is a integer +// and equals the value passed in +func peekTopEStackIsValue(t *testing.T, vm *VM, value int64) bool { + item := peakTopEstack(t, vm) + integer, err := item.Integer() + assert.Nil(t, err) + return value == integer.Value().Int64() +} + +// peaks the stack item on the top of the evaluation stack +// if the current context and returns it +func peakTopEstack(t *testing.T, vm *VM) stack.Item { + ctx, err := vm.InvocationStack.CurrentContext() + fmt.Println(err) + assert.Nil(t, err) + item, err := ctx.Estack.Peek(0) + assert.Nil(t, err) + return item +} + +// returns true if the total number of items on the evaluation stack is equal to value +func EstackLen(t *testing.T, vm *VM, value int) bool { + ctx, err := vm.InvocationStack.CurrentContext() + assert.Nil(t, err) + return value == ctx.Estack.Len() +}