Cross platform virtual machine implementation (#60)

* Virtual machine for the NEO blockhain.

* fixed big.Int numeric operation pointer issue.

* added appcall

* Added README for vm package.

* removed main.go

* started VM cli (prompt) integration

* added support for printing the stack.

* moved cli to vm package

* fixed vet errors

* updated readme

* added more test for VM and fixed some edge cases.

* bumped version -> 0.37.0
This commit is contained in:
Anthony De Meulemeester 2018-03-30 18:15:06 +02:00 committed by GitHub
parent 0b023c5c5c
commit 931388b687
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1914 additions and 3 deletions

View file

@ -1 +1 @@
0.36.0
0.37.0

View file

@ -5,6 +5,7 @@ import (
"github.com/CityOfZion/neo-go/cli/server"
"github.com/CityOfZion/neo-go/cli/smartcontract"
"github.com/CityOfZion/neo-go/cli/vm"
"github.com/CityOfZion/neo-go/cli/wallet"
"github.com/urfave/cli"
)
@ -18,6 +19,7 @@ func main() {
server.NewCommand(),
smartcontract.NewCommand(),
wallet.NewCommand(),
vm.NewCommand(),
}
ctl.Run(os.Args)

23
cli/vm/vm.go Normal file
View file

@ -0,0 +1,23 @@
package vm
import (
vmcli "github.com/CityOfZion/neo-go/pkg/vm/cli"
"github.com/urfave/cli"
)
// NewCommand creates a new VM command.
func NewCommand() cli.Command {
return cli.Command{
Name: "vm",
Usage: "start the virtual machine",
Action: startVMPrompt,
Flags: []cli.Flag{
cli.BoolFlag{Name: "debug, d"},
},
}
}
func startVMPrompt(ctx *cli.Context) error {
p := vmcli.New()
return p.Run()
}

View file

@ -17,7 +17,9 @@ func TestDecodeEncodeAccountState(t *testing.T) {
)
for i := 0; i < n; i++ {
balances[util.RandomUint256()] = util.Fixed8(int64(util.RandomInt(1, 10000)))
votes[i] = &crypto.PublicKey{crypto.RandomECPoint()}
votes[i] = &crypto.PublicKey{
ECPoint: crypto.RandomECPoint(),
}
}
a := &AccountState{

140
pkg/vm/README.md Normal file
View file

@ -0,0 +1,140 @@
# NEO-GO-VM
A cross platform virtual machine implementation for `avm` compatible programs.
# Installation
## With neo-go
Install dependencies.
`neo-go` uses [dep](https://github.com/golang/dep) as its dependency manager. After installing `deps` you can run:
```
make deps
```
Build the `neo-go` cli:
```
make build
```
Start the virtual machine:
```
./bin/neo-go vm
```
```
_ ____________ __________ _ ____ ___
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /
/ |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ /
/ /| / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / / / /
/_/ |_/_____/\____/ \____/\____/ |___/_/ /_/
NEO-GO-VM >
```
## Standalone
More information about standalone installation coming soon.
# Usage
```
_ ____________ __________ _ ____ ___
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /
/ |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ /
/ /| / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / / / /
/_/ |_/_____/\____/ \____/\____/ |___/_/ /_/
NEO-GO-VM > help
COMMAND USAGE
run execute the current loaded script
exit exit the VM prompt
estack shows evaluation stack details
break place a breakpoint (> break 1)
astack shows alt stack details
istack show invocation stack details
load load a script into the VM (> load /path/to/script.avm)
resume resume the current loaded script
step step (n) instruction in the program (> step 10)
help show available commands
ip show the current instruction
opcode print the opcodes of the current loaded program
```
### Loading in your script
To load a script into the VM:
```
NEO-GO-VM > load ../contract.avm
READY
```
Run the script:
```
NEO-GO-VM > run
[
{
"value": 1,
"type": "BigInteger"
}
]
```
### Debugging
The `neo-go-vm` provides a debugger to inspect your program in-depth.
Step 4 instructions.
```
NEO-GO-VM > step 4
at breakpoint 4 (Opush4)
```
Using just `step` will execute 1 instruction at a time.
```
NEO-GO-VM > step
instruction pointer at 5 (Odup)
```
To place breakpoints:
```
NEO-GO-VM > break 10
breakpoint added at instruction 10
NEO-GO-VM > resume
at breakpoint 10 (Osetitem)
```
Inspecting the stack:
```
NEO-GO-VM > stack
[
{
"value": [
null,
null,
null,
null,
null,
null,
null
],
"type": "Array"
},
{
"value": 4,
"type": "BigInteger"
}
]
```
And a lot more features coming next weeks..

168
pkg/vm/cli/cli.go Normal file
View file

@ -0,0 +1,168 @@
package cli
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/CityOfZion/neo-go/pkg/vm"
)
// command describes a VM command.
type command struct {
// number of minimun arguments the command needs.
args int
// description of the command.
usage string
// whether the VM needs to be "ready" to execute this command.
ready bool
}
var commands = map[string]command{
"help": {0, "show available commands", false},
"exit": {0, "exit the VM prompt", false},
"ip": {0, "show the current instruction", true},
"break": {1, "place a breakpoint (> break 1)", true},
"estack": {0, "shows evaluation stack details", false},
"astack": {0, "shows alt stack details", false},
"istack": {0, "show invocation stack details", false},
"load": {1, "load a script into the VM (> load /path/to/script.avm)", false},
"run": {0, "execute the current loaded script", true},
"resume": {0, "resume the current loaded script", true},
"step": {0, "step (n) instruction in the program (> step 10)", true},
"opcode": {0, "print the opcodes of the current loaded program", true},
}
// VMCLI object for interacting with the VM.
type VMCLI struct {
vm *vm.VM
}
// New returns a new VMCLI object.
func New() *VMCLI {
return &VMCLI{
vm: vm.New(nil),
}
}
func (c *VMCLI) handleCommand(cmd string, args ...string) {
com, ok := commands[cmd]
if !ok {
fmt.Printf("unknown command (%s)\n", cmd)
return
}
if len(args) < com.args {
fmt.Printf("command (%s) takes at least %d arguments\n", cmd, com.args)
return
}
if com.ready && !c.vm.Ready() {
fmt.Println("VM is not ready: no program loaded")
return
}
switch cmd {
case "help":
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
fmt.Fprintln(w, "COMMAND\tUSAGE")
for name, details := range commands {
fmt.Fprintf(w, "%s\t%s\n", name, details.usage)
}
w.Flush()
fmt.Println()
case "exit":
fmt.Println("Bye!")
os.Exit(0)
case "ip":
ip, opcode := c.vm.Context().CurrInstr()
fmt.Printf("instruction pointer at %d (%s)\n", ip, opcode)
case "break":
n, err := strconv.Atoi(args[0])
if err != nil {
fmt.Printf("argument conversion error: %s\n", err)
return
}
c.vm.AddBreakPoint(n)
fmt.Printf("breakpoint added at instruction %d\n", n)
case "estack", "istack", "astack":
fmt.Println(c.vm.Stack(cmd))
case "load":
if err := c.vm.Load(args[0]); err != nil {
fmt.Println(err)
} else {
fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr())
}
case "run", "resume":
c.vm.Run()
case "step":
var (
n = 1
err error
)
if len(args) > 0 {
n, err = strconv.Atoi(args[0])
if err != nil {
fmt.Printf("argument conversion error: %s\n", err)
return
}
}
c.vm.AddBreakPointRel(n)
c.vm.Run()
case "opcode":
prog := c.vm.Context().Program()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
fmt.Fprintln(w, "INDEX\tOPCODE\tDESC\t")
for i := 0; i < len(prog); i++ {
fmt.Fprintf(w, "%d\t0x%2x\t%s\t\n", i, prog[i], vm.Opcode(prog[i]))
}
w.Flush()
}
}
// Run waits for user input from Stdin and executes the passed command.
func (c *VMCLI) Run() error {
printLogo()
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("NEO-GO-VM > ")
input, _ := reader.ReadString('\n')
input = strings.Trim(input, "\n")
if len(input) != 0 {
parts := strings.Split(input, " ")
cmd := parts[0]
args := []string{}
if len(parts) > 1 {
args = parts[1:]
}
c.handleCommand(cmd, args...)
}
}
}
func printLogo() {
logo := `
_ ____________ __________ _ ____ ___
/ | / / ____/ __ \ / ____/ __ \ | | / / |/ /
/ |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ /
/ /| / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / / / /
/_/ |_/_____/\____/ \____/\____/ |___/_/ /_/
`
fmt.Print(logo)
fmt.Println()
fmt.Println()
}

123
pkg/vm/context.go Normal file
View file

@ -0,0 +1,123 @@
package vm
import "encoding/binary"
// Context represent the current execution context of the VM.
type Context struct {
// Instruction pointer.
ip int
// The raw program script.
prog []byte
// Breakpoints
breakPoints []int
}
// 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() Opcode {
c.ip++
return Opcode(c.prog[c.ip])
}
// IP returns the absosulute 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, Opcode) {
if c.ip < 0 {
return c.ip, Opcode(0x00)
}
return c.ip, Opcode(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
}
// Value implements StackItem interface.
func (c *Context) Value() interface{} {
return c
}
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"
}
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
}
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
}
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))
}

32
pkg/vm/interop.go Normal file
View file

@ -0,0 +1,32 @@
package vm
import "fmt"
// InteropFunc allows to hook into the VM.
type InteropFunc func(vm *VM) error
// InteropService
type InteropService struct {
mapping map[string]InteropFunc
}
// NewInteropService returns a new InteropService object.
func NewInteropService() *InteropService {
return &InteropService{
mapping: map[string]InteropFunc{},
}
}
// Register any API to the interop service.
func (i *InteropService) Register(api string, fun InteropFunc) {
i.mapping[api] = fun
}
// Call will invoke the service mapped to the given api.
func (i *InteropService) Call(api []byte, vm *VM) error {
fun, ok := i.mapping[string(api)]
if !ok {
return fmt.Errorf("api (%s) not in interop mapping", api)
}
return fun(vm)
}

223
pkg/vm/opcode_test.go Normal file
View file

@ -0,0 +1,223 @@
package vm
import (
"bytes"
"encoding/hex"
"math/rand"
"testing"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestPushBytes1to75(t *testing.T) {
buf := new(bytes.Buffer)
for i := 1; i <= 75; i++ {
b := randomBytes(i)
EmitBytes(buf, b)
vm := load(buf.Bytes())
vm.Step()
assert.Equal(t, 1, vm.estack.Len())
elem := vm.estack.Pop()
assert.IsType(t, &byteArrayItem{}, elem.value)
assert.IsType(t, elem.Bytes(), b)
assert.Equal(t, 0, vm.estack.Len())
vm.execute(nil, Oret)
assert.Equal(t, 0, vm.astack.Len())
assert.Equal(t, 0, vm.istack.Len())
buf.Reset()
}
}
func TestPushm1to16(t *testing.T) {
prog := []byte{}
for i := int(Opushm1); i <= int(Opush16); i++ {
if i == 80 {
continue // opcode layout we got here.
}
prog = append(prog, byte(i))
}
vm := load(prog)
for i := int(Opushm1); i <= int(Opush16); i++ {
if i == 80 {
continue // nice opcode layout we got here.
}
vm.Step()
elem := vm.estack.Pop()
assert.IsType(t, &bigIntegerItem{}, elem.value)
val := i - int(Opush1) + 1
assert.Equal(t, elem.BigInt().Int64(), int64(val))
}
}
func TestPushData1(t *testing.T) {
}
func TestPushData2(t *testing.T) {
}
func TestPushData4(t *testing.T) {
}
func TestAdd(t *testing.T) {
prog := makeProgram(Oadd)
vm := load(prog)
vm.estack.PushVal(4)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, int64(6), vm.estack.Pop().BigInt().Int64())
}
func TestMul(t *testing.T) {
prog := makeProgram(Omul)
vm := load(prog)
vm.estack.PushVal(4)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, int64(8), vm.estack.Pop().BigInt().Int64())
}
func TestDiv(t *testing.T) {
prog := makeProgram(Odiv)
vm := load(prog)
vm.estack.PushVal(4)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, int64(2), vm.estack.Pop().BigInt().Int64())
}
func TestSub(t *testing.T) {
prog := makeProgram(Osub)
vm := load(prog)
vm.estack.PushVal(4)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, int64(2), vm.estack.Pop().BigInt().Int64())
}
func TestLT(t *testing.T) {
prog := makeProgram(Olt)
vm := load(prog)
vm.estack.PushVal(4)
vm.estack.PushVal(3)
vm.Run()
assert.Equal(t, false, vm.estack.Pop().Bool())
}
func TestLTE(t *testing.T) {
prog := makeProgram(Olte)
vm := load(prog)
vm.estack.PushVal(2)
vm.estack.PushVal(3)
vm.Run()
assert.Equal(t, true, vm.estack.Pop().Bool())
}
func TestGT(t *testing.T) {
prog := makeProgram(Ogt)
vm := load(prog)
vm.estack.PushVal(9)
vm.estack.PushVal(3)
vm.Run()
assert.Equal(t, true, vm.estack.Pop().Bool())
}
func TestGTE(t *testing.T) {
prog := makeProgram(Ogte)
vm := load(prog)
vm.estack.PushVal(3)
vm.estack.PushVal(3)
vm.Run()
assert.Equal(t, true, vm.estack.Pop().Bool())
}
func TestDepth(t *testing.T) {
prog := makeProgram(Odepth)
vm := load(prog)
vm.estack.PushVal(1)
vm.estack.PushVal(2)
vm.estack.PushVal(3)
vm.Run()
assert.Equal(t, int64(3), vm.estack.Pop().BigInt().Int64())
}
func TestNumEqual(t *testing.T) {
prog := makeProgram(Onumequal)
vm := load(prog)
vm.estack.PushVal(1)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, false, vm.estack.Pop().Bool())
}
func TestNumNotEqual(t *testing.T) {
prog := makeProgram(Onumnotequal)
vm := load(prog)
vm.estack.PushVal(2)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, false, vm.estack.Pop().Bool())
}
func TestAppCall(t *testing.T) {
prog := []byte{byte(Oappcall)}
hash := util.Uint160{}
prog = append(prog, hash.Bytes()...)
prog = append(prog, byte(Oret))
vm := load(prog)
vm.scripts[hash] = makeProgram(Odepth)
vm.estack.PushVal(2)
vm.Run()
elem := vm.estack.Pop() // depth should be 1
assert.Equal(t, int64(1), elem.BigInt().Int64())
}
func TestSimpleCall(t *testing.T) {
progStr := "52c56b525a7c616516006c766b00527ac46203006c766b00c3616c756653c56b6c766b00527ac46c766b51527ac46203006c766b00c36c766b51c393616c7566"
result := 12
prog, err := hex.DecodeString(progStr)
if err != nil {
t.Fatal(err)
}
vm := load(prog)
vm.Run()
assert.Equal(t, result, int(vm.estack.Pop().BigInt().Int64()))
}
func makeProgram(opcodes ...Opcode) []byte {
prog := make([]byte, len(opcodes)+1) // Oret
for i := 0; i < len(opcodes); i++ {
prog[i] = byte(opcodes[i])
}
prog[len(prog)-1] = byte(Oret)
return prog
}
func load(prog []byte) *VM {
vm := New(nil)
vm.mute = true
vm.istack.PushVal(NewContext(prog))
return vm
}
func randomBytes(n int) []byte {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return b
}

25
pkg/vm/output.go Normal file
View file

@ -0,0 +1,25 @@
package vm
import "encoding/json"
// StackOutput holds information about the stack, used for pretty printing
// the stack.
type stackItem struct {
Value interface{} `json:"value"`
Type string `json:"type"`
}
func buildStackOutput(s *Stack) string {
items := make([]stackItem, s.Len())
i := 0
s.Iter(func(e *Element) {
items[i] = stackItem{
Value: e.value.Value(),
Type: e.value.String(),
}
i++
})
b, _ := json.MarshalIndent(items, "", " ")
return string(b)
}

View file

@ -55,7 +55,7 @@ func EmitString(w *bytes.Buffer, s string) error {
return EmitBytes(w, []byte(s))
}
// EmitBytes emits a byte array the given buffer.
// EmitBytes emits a byte array to the given buffer.
func EmitBytes(w *bytes.Buffer, b []byte) error {
var (
err error

234
pkg/vm/stack.go Normal file
View file

@ -0,0 +1,234 @@
package vm
import (
"math/big"
"github.com/CityOfZion/neo-go/pkg/util"
)
// Stack implementation for the neo-go virtual machine. The stack implements
// a double linked list where its semantics are first in first out.
// To simplify the implementation, internally a Stack s is implemented as a
// ring, such that &s.top is both the next element of the last element s.Back()
// and the previous element of the first element s.Top().
//
// s.Push(0)
// s.Push(1)
// s.Push(2)
//
// [ 2 ] > top
// [ 1 ]
// [ 0 ] > back
//
// s.Pop() > 2
//
// [ 1 ]
// [ 0 ]
// Element represents an element in the double linked list (the stack),
// which will hold the underlying StackItem.
type Element struct {
value StackItem
next, prev *Element
stack *Stack
}
// NewElement returns a new Element object, with its underlying value infered
// to the corresponding type.
func NewElement(v interface{}) *Element {
return &Element{
value: makeStackItem(v),
}
}
// Next returns the next element in the stack.
func (e *Element) Next() *Element {
if elem := e.next; e.stack != nil && elem != &e.stack.top {
return elem
}
return nil
}
// Prev returns the previous element in the stack.
func (e *Element) Prev() *Element {
if elem := e.prev; e.stack != nil && elem != &e.stack.top {
return elem
}
return nil
}
// BigInt attempts to get the underlying value of the element as a big integer.
// Will panic if the assertion failed which will be catched by the VM.
func (e *Element) BigInt() *big.Int {
switch t := e.value.(type) {
case *bigIntegerItem:
return t.value
default:
b := t.Value().([]uint8)
return new(big.Int).SetBytes(util.ArrayReverse(b))
}
}
// Bool attempts to get the underlying value of the element as a boolean.
// Will panic if the assertion failed which will be catched by the VM.
func (e *Element) Bool() bool {
return e.value.Value().(bool)
}
// Bytes attempts to get the underlying value of the element as a byte array.
// Will panic if the assertion failed which will be catched by the VM.
func (e *Element) Bytes() []byte {
return e.value.Value().([]byte)
}
// Stack represents a Stack backed by a double linked list.
type Stack struct {
top Element
name string
len int
}
// NewStack returns a new stack name by the given name.
func NewStack(n string) *Stack {
s := &Stack{
name: n,
}
s.top.next = &s.top
s.top.prev = &s.top
s.len = 0
return s
}
// Len return the number of elements that are on the stack.
func (s *Stack) Len() int {
return s.len
}
// insert will insert the element after element (at) on the stack.
func (s *Stack) insert(e, at *Element) *Element {
// If we insert an element that is already popped from this stack,
// we need to clean it up, there are still pointers referencing to it.
if e.stack == s {
e = NewElement(e.value)
}
n := at.next
at.next = e
e.prev = at
e.next = n
n.prev = e
e.stack = s
s.len++
return e
}
// InsertBefore will insert the element before the mark on the stack.
func (s *Stack) InsertBefore(e, mark *Element) *Element {
if mark == nil {
return nil
}
return s.insert(e, mark.prev)
}
// InsertAt will insert the given item (n) deep on the stack.
func (s *Stack) InsertAt(e *Element, n int) *Element {
before := s.Peek(n)
if before == nil {
return nil
}
return s.InsertBefore(e, before)
}
// Push pushes the given element on the stack.
func (s *Stack) Push(e *Element) {
s.insert(e, &s.top)
}
// PushVal will push the given value on the stack. It will infer the
// underlying StackItem to its corresponding type.
func (s *Stack) PushVal(v interface{}) {
s.Push(NewElement(v))
}
// Pop removes and returns the element on top of the stack.
func (s *Stack) Pop() *Element {
return s.Remove(s.Top())
}
// Top returns the element on top of the stack. Nil if the stack
// is empty.
func (s *Stack) Top() *Element {
if s.len == 0 {
return nil
}
return s.top.next
}
// Back returns the element at the end of the stack. Nil if the stack
// is empty.
func (s *Stack) Back() *Element {
if s.len == 0 {
return nil
}
return s.top.prev
}
// Peek returns the element (n) far in the stack beginning from
// the top of the stack.
// n = 0 => will return the element on top of the stack.
func (s *Stack) Peek(n int) *Element {
i := 0
for e := s.Top(); e != nil; e = e.Next() {
if n == i {
return e
}
i++
}
return nil
}
// RemoveAt removes the element (n) deep on the stack beginning
// from the top of the stack.
func (s *Stack) RemoveAt(n int) *Element {
return s.Remove(s.Peek(n))
}
// Remove removes and returns the given element from the stack.
func (s *Stack) Remove(e *Element) *Element {
if e == nil {
return nil
}
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks.
e.prev = nil // avoid memory leaks.
e.stack = nil
s.len--
return e
}
// Dup will duplicate and return the element at position n.
// Dup is used for copying elements on to the top of its own stack.
// s.Push(s.Peek(0)) // will result in unexpected behaviour.
// s.Push(s.Dup(0)) // is the correct approach.
func (s *Stack) Dup(n int) *Element {
e := s.Peek(n)
if e == nil {
return nil
}
return &Element{
value: e.value,
}
}
// Iter will iterate over all the elements int the stack, starting from the top
// of the stack.
// s.Iter(func(elem *Element) {
// // do something with the element.
// })
func (s *Stack) Iter(f func(*Element)) {
for e := s.Top(); e != nil; e = e.Next() {
f(e)
}
}

113
pkg/vm/stack_item.go Normal file
View file

@ -0,0 +1,113 @@
package vm
import (
"fmt"
"math/big"
"reflect"
)
// A StackItem represents the "real" value that is pushed on the stack.
type StackItem interface {
fmt.Stringer
Value() interface{}
}
func makeStackItem(v interface{}) StackItem {
switch val := v.(type) {
case int:
return &bigIntegerItem{
value: big.NewInt(int64(val)),
}
case []byte:
return &byteArrayItem{
value: val,
}
case bool:
return &boolItem{
value: val,
}
case []StackItem:
return &arrayItem{
value: val,
}
case *big.Int:
return &bigIntegerItem{
value: val,
}
case StackItem:
return val
default:
panic(
fmt.Sprintf(
"invalid stack item type: %v (%s)",
val,
reflect.TypeOf(val),
),
)
}
}
type structItem struct {
value []StackItem
}
// Value implements StackItem interface.
func (i *structItem) Value() interface{} {
return i.value
}
func (i *structItem) String() string {
return "Struct"
}
type bigIntegerItem struct {
value *big.Int
}
// Value implements StackItem interface.
func (i *bigIntegerItem) Value() interface{} {
return i.value
}
func (i *bigIntegerItem) String() string {
return "BigInteger"
}
type boolItem struct {
value bool
}
// Value implements StackItem interface.
func (i *boolItem) Value() interface{} {
return i.value
}
func (i *boolItem) String() string {
return "Bool"
}
type byteArrayItem struct {
value []byte
}
// Value implements StackItem interface.
func (i *byteArrayItem) Value() interface{} {
return i.value
}
func (i *byteArrayItem) String() string {
return "ByteArray"
}
type arrayItem struct {
value []StackItem
}
// Value implements StackItem interface.
func (i *arrayItem) Value() interface{} {
return i.value
}
func (i *arrayItem) String() string {
return "Array"
}

182
pkg/vm/stack_test.go Normal file
View file

@ -0,0 +1,182 @@
package vm
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPushElement(t *testing.T) {
elems := makeElements(10)
s := NewStack("test")
for _, elem := range elems {
s.Push(elem)
}
assert.Equal(t, len(elems), s.Len())
for i := 0; i < len(elems); i++ {
assert.Equal(t, elems[len(elems)-1-i], s.Peek(i))
}
}
func TestPopElement(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
for i := len(elems) - 1; i >= 0; i-- {
assert.Equal(t, elems[i], s.Pop())
assert.Equal(t, i, s.Len())
}
}
func TestPeekElement(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
for i := len(elems) - 1; i >= 0; i-- {
assert.Equal(t, elems[i], s.Peek(len(elems)-i-1))
}
}
func TestRemoveAt(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
elem := s.RemoveAt(8)
assert.Equal(t, elems[1], elem)
assert.Nil(t, elem.prev)
assert.Nil(t, elem.next)
assert.Nil(t, elem.stack)
// Test if the pointers are moved.
assert.Equal(t, elems[0], s.Peek(8))
assert.Equal(t, elems[2], s.Peek(7))
}
func TestPushFromOtherStack(t *testing.T) {
var (
s1 = NewStack("test")
s2 = NewStack("test2")
elems = makeElements(2)
)
for _, elem := range elems {
s1.Push(elem)
}
s2.Push(NewElement(100))
s2.Push(NewElement(101))
s1.Push(s2.Pop())
assert.Equal(t, len(elems)+1, s1.Len())
assert.Equal(t, 1, s2.Len())
}
func TestDupElement(t *testing.T) {
s := NewStack("test")
elemA := NewElement(101)
s.Push(elemA)
dupped := s.Dup(0)
s.Push(dupped)
assert.Equal(t, 2, s.Len())
assert.Equal(t, dupped, s.Peek(0))
}
func TestBack(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
assert.Equal(t, elems[0], s.Back())
}
func TestTop(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
assert.Equal(t, elems[len(elems)-1], s.Top())
}
func TestRemoveLastElement(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(2)
)
for _, elem := range elems {
s.Push(elem)
}
elem := s.RemoveAt(1)
assert.Equal(t, elems[0], elem)
assert.Nil(t, elem.prev)
assert.Nil(t, elem.next)
assert.Equal(t, 1, s.Len())
}
func TestIterAfterRemove(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
s.RemoveAt(0)
i := 0
s.Iter(func(elem *Element) {
i++
})
assert.Equal(t, len(elems)-1, i)
}
func TestIteration(t *testing.T) {
var (
s = NewStack("test")
elems = makeElements(10)
)
for _, elem := range elems {
s.Push(elem)
}
assert.Equal(t, len(elems), s.Len())
i := 0
s.Iter(func(elem *Element) {
i++
})
assert.Equal(t, len(elems), i)
}
func TestPushVal(t *testing.T) {
}
func makeElements(n int) []*Element {
elems := make([]*Element, n)
for i := 0; i < n; i++ {
elems[i] = NewElement(i)
}
return elems
}

25
pkg/vm/state.go Normal file
View file

@ -0,0 +1,25 @@
package vm
// State of the VM.
type State uint
// Available States.
const (
noneState State = iota
haltState
faultState
breakState
)
func (s State) String() string {
switch s {
case haltState:
return "HALT"
case faultState:
return "FAULT"
case breakState:
return "BREAK"
default:
return "NONE"
}
}

619
pkg/vm/vm.go Normal file
View file

@ -0,0 +1,619 @@
package vm
import (
"crypto/sha1"
"crypto/sha256"
"fmt"
"io/ioutil"
"log"
"math/big"
"github.com/CityOfZion/neo-go/pkg/util"
"golang.org/x/crypto/ripemd160"
)
// VM represents the virtual machine.
type VM struct {
state State
// interop layer.
interop *InteropService
// scripts loaded in memory.
scripts map[util.Uint160][]byte
istack *Stack // invocation stack.
estack *Stack // execution stack.
astack *Stack // alt stack.
// Mute all output after execution.
mute bool
}
// New returns a new VM object ready to load .avm bytecode scripts.
func New(svc *InteropService) *VM {
if svc == nil {
svc = NewInteropService()
}
return &VM{
interop: svc,
scripts: make(map[util.Uint160][]byte),
state: haltState,
istack: NewStack("invocation"),
estack: NewStack("evaluation"),
astack: NewStack("alt"),
}
}
// AddBreakPoint adds a breakpoint to the current context.
func (v *VM) AddBreakPoint(n int) {
ctx := v.Context()
ctx.breakPoints = append(ctx.breakPoints, n)
}
// AddBreakPointRel adds a breakpoint relative to the current
// instruction pointer.
func (v *VM) AddBreakPointRel(n int) {
ctx := v.Context()
v.AddBreakPoint(ctx.ip + n)
}
// Load will load a program from the given path, ready to execute it.
func (v *VM) Load(path string) error {
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
v.istack.PushVal(NewContext(b))
return nil
}
// LoadScript will load a script from the internal script table. It
// will immediatly push a new context created from this script to
// the invocation stack and starts executing it.
func (v *VM) LoadScript(b []byte) {
ctx := NewContext(b)
v.istack.PushVal(ctx)
}
// Context returns the current executed context. Nil if there is no context,
// which implies no program is loaded.
func (v *VM) Context() *Context {
if v.istack.Len() == 0 {
return nil
}
return v.istack.Peek(0).value.Value().(*Context)
}
// Stack returns json formatted representation of the given stack.
func (v *VM) Stack(n string) string {
var s *Stack
if n == "astack" {
s = v.astack
}
if n == "istack" {
s = v.istack
}
if n == "estack" {
s = v.estack
}
return buildStackOutput(s)
}
// Ready return true if the VM ready to execute the loaded program.
// Will return false if no program is loaded.
func (v *VM) Ready() bool {
return v.istack.Len() > 0
}
// Run starts the execution of the loaded program.
func (v *VM) Run() {
if !v.Ready() {
fmt.Println("no program loaded")
return
}
v.state = noneState
for {
switch v.state {
case haltState:
if !v.mute {
fmt.Println(v.Stack("estack"))
}
return
case breakState:
ctx := v.Context()
i, op := ctx.CurrInstr()
fmt.Printf("at breakpoint %d (%s)\n", i, op)
return
case faultState:
fmt.Println("FAULT")
return
case noneState:
v.Step()
}
}
}
// Step 1 instruction in the program.
func (v *VM) Step() {
ctx := v.Context()
op := ctx.Next()
if ctx.ip >= len(ctx.prog) {
op = Oret
}
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
}
}
// execute performs an instruction cycle in the VM. Acting on the instruction (opcode).
func (v *VM) execute(ctx *Context, op Opcode) {
// Instead of poluting 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() {
if err := recover(); err != nil {
log.Printf("error encountered at instruction %d (%s)", ctx.ip, op)
log.Println(err)
v.state = faultState
}
}()
if op >= Opushbytes1 && op <= Opushbytes75 {
b := ctx.readBytes(int(op))
v.estack.PushVal(b)
return
}
switch op {
case Opushm1, Opush1, Opush2, Opush3, Opush4, Opush5,
Opush6, Opush7, Opush8, Opush9, Opush10, Opush11,
Opush12, Opush13, Opush14, Opush15, Opush16:
val := int(op) - int(Opush1) + 1
v.estack.PushVal(val)
case Opush0:
v.estack.PushVal(0)
case Opushdata1:
n := ctx.readByte()
b := ctx.readBytes(int(n))
v.estack.PushVal(b)
case Opushdata2:
n := ctx.readUint16()
b := ctx.readBytes(int(n))
v.estack.PushVal(b)
case Opushdata4:
n := ctx.readUint32()
b := ctx.readBytes(int(n))
v.estack.PushVal(b)
// Stack operations.
case Otoaltstack:
v.astack.Push(v.estack.Pop())
case Ofromaltstack:
v.estack.Push(v.astack.Pop())
case Odupfromaltstack:
v.estack.Push(v.astack.Dup(0))
case Odup:
v.estack.Push(v.estack.Dup(0))
case Oswap:
a := v.estack.Pop()
b := v.estack.Pop()
v.estack.Push(a)
v.estack.Push(b)
case Oxswap:
n := int(v.estack.Pop().BigInt().Int64())
if n < 0 {
panic("XSWAP: invalid length")
}
// Swap values of elements instead of reordening stack elements.
if n > 0 {
a := v.estack.Peek(n)
b := v.estack.Peek(0)
aval := a.value
bval := b.value
a.value = bval
b.value = aval
}
case Otuck:
n := int(v.estack.Pop().BigInt().Int64())
if n <= 0 {
panic("OTUCK: invalid length")
}
v.estack.InsertAt(v.estack.Peek(0), n)
case Odepth:
v.estack.PushVal(v.estack.Len())
case Onip:
elem := v.estack.Pop()
_ = v.estack.Pop()
v.estack.Push(elem)
case Oover:
b := v.estack.Pop()
a := v.estack.Peek(0)
v.estack.Push(b)
v.estack.Push(a)
case Oroll:
n := int(v.estack.Pop().BigInt().Int64())
if n < 0 {
panic("negative stack item returned")
}
if n > 0 {
v.estack.Push(v.estack.RemoveAt(n - 1))
}
case Odrop:
v.estack.Pop()
case Oequal:
panic("TODO EQUAL")
// Bit operations.
case Oand:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).And(b, a))
case Oor:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Or(b, a))
case Oxor:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Xor(b, a))
// Numeric operations.
case Oadd:
a := v.estack.Pop().BigInt()
b := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Add(a, b))
case Osub:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Sub(a, b))
case Odiv:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Div(a, b))
case Omul:
a := v.estack.Pop().BigInt()
b := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Mul(a, b))
case Omod:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Mod(a, b))
case Oshl:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Lsh(a, uint(b.Int64())))
case Oshr:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Rsh(a, uint(b.Int64())))
case Obooland:
b := v.estack.Pop().Bool()
a := v.estack.Pop().Bool()
v.estack.PushVal(a && b)
case Oboolor:
b := v.estack.Pop().Bool()
a := v.estack.Pop().Bool()
v.estack.PushVal(a || b)
case Onumequal:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(b) == 0)
case Onumnotequal:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(b) != 0)
case Olt:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(b) == -1)
case Ogt:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(b) == 1)
case Olte:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(b) <= 0)
case Ogte:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(b) >= 0)
case Omin:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
val := a
if a.Cmp(b) == 1 {
val = b
}
v.estack.PushVal(val)
case Omax:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
val := a
if a.Cmp(b) == -1 {
val = b
}
v.estack.PushVal(val)
case Owithin:
b := v.estack.Pop().BigInt()
a := v.estack.Pop().BigInt()
x := v.estack.Pop().BigInt()
v.estack.PushVal(a.Cmp(x) <= 0 && x.Cmp(b) == -1)
case Oinc:
x := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Add(x, big.NewInt(1)))
case Odec:
x := v.estack.Pop().BigInt()
v.estack.PushVal(new(big.Int).Sub(x, big.NewInt(1)))
case Osign:
x := v.estack.Pop().BigInt()
v.estack.PushVal(x.Sign())
case Onegate:
x := v.estack.Pop().BigInt()
v.estack.PushVal(x.Neg(x))
case Oabs:
x := v.estack.Pop().BigInt()
v.estack.PushVal(x.Abs(x))
case Onot:
x := v.estack.Pop().BigInt()
v.estack.PushVal(x.Not(x))
case Onz:
panic("todo NZ")
// x := v.estack.Pop().BigInt()
// Object operations.
case Onewarray:
n := v.estack.Pop().BigInt().Int64()
items := make([]StackItem, n)
v.estack.PushVal(&arrayItem{items})
case Onewstruct:
n := v.estack.Pop().BigInt().Int64()
items := make([]StackItem, n)
v.estack.PushVal(&structItem{items})
case Oappend:
itemElem := v.estack.Pop()
arrElem := v.estack.Pop()
switch t := arrElem.value.(type) {
case *arrayItem, *structItem:
arr := t.Value().([]StackItem)
arr = append(arr, itemElem.value)
default:
panic("APPEND: not of underlying type Array")
}
case Oreverse:
case Oremove:
case Opack:
n := int(v.estack.Pop().BigInt().Int64())
if n < 0 || n > v.estack.Len() {
panic("OPACK: invalid length")
}
items := make([]StackItem, n)
for i := 0; i < n; i++ {
items[i] = v.estack.Pop().value
}
v.estack.PushVal(items)
case Ounpack:
panic("TODO")
case Opickitem:
var (
key = v.estack.Pop()
obj = v.estack.Pop()
index = int(key.BigInt().Int64())
)
switch t := obj.value.(type) {
// Struct and Array items have their underlying value as []StackItem.
case *arrayItem, *structItem:
arr := t.Value().([]StackItem)
if index < 0 || index >= len(arr) {
panic("PICKITEM: invalid index")
}
item := arr[index]
v.estack.PushVal(item)
default:
panic("PICKITEM: unknown type")
}
case Osetitem:
var (
obj = v.estack.Pop()
key = v.estack.Pop()
item = v.estack.Pop().value
index = int(key.BigInt().Int64())
)
switch t := obj.value.(type) {
// Struct and Array items have their underlying value as []StackItem.
case *arrayItem, *structItem:
arr := t.Value().([]StackItem)
if index < 0 || index >= len(arr) {
panic("PICKITEM: invalid index")
}
arr[index] = item
default:
panic("SETITEM: unknown type")
}
case Oarraysize:
elem := v.estack.Pop()
arr, ok := elem.value.Value().([]StackItem)
if !ok {
panic("ARRAYSIZE: item not of type []StackItem")
}
v.estack.PushVal(len(arr))
case Ojmp, Ojmpif, Ojmpifnot:
rOffset := ctx.readUint16()
offset := ctx.ip + int(rOffset) - 3 // sizeOf(uint16 + uint8)
if offset < 0 || offset > len(ctx.prog) {
panic("JMP: invalid offset")
}
cond := true
if op > Ojmp {
cond = v.estack.Pop().Bool()
if op == Ojmpifnot {
cond = !cond
}
}
if cond {
ctx.ip = offset
}
case Ocall:
v.istack.PushVal(ctx.Copy())
ctx.ip += 2
v.execute(v.Context(), Ojmp)
case Osyscall:
api := ctx.readVarBytes()
err := v.interop.Call(api, v)
if err != nil {
panic(fmt.Sprintf("failed to invoke syscall: %s", err))
}
case Oappcall, Otailcall:
if len(v.scripts) == 0 {
panic("script table is empty")
}
hash, err := util.Uint160DecodeBytes(ctx.readBytes(20))
if err != nil {
panic(err)
}
script, ok := v.scripts[hash]
if !ok {
panic("could not find script")
}
if op == Otailcall {
_ = v.istack.Pop()
}
v.LoadScript(script)
case Oret:
_ = v.istack.Pop()
if v.istack.Len() == 0 {
v.state = haltState
}
// Cryptographic operations.
case Osha1:
b := v.estack.Pop().Bytes()
sha := sha1.New()
sha.Write(b)
v.estack.PushVal(sha.Sum(nil))
case Osha256:
b := v.estack.Pop().Bytes()
sha := sha256.New()
sha.Write(b)
v.estack.PushVal(sha.Sum(nil))
case Ohash160:
b := v.estack.Pop().Bytes()
sha := sha256.New()
sha.Write(b)
h := sha.Sum(nil)
ripemd := ripemd160.New()
ripemd.Write(h)
v.estack.PushVal(ripemd.Sum(nil))
case Ohash256:
b := v.estack.Pop().Bytes()
sha := sha256.New()
sha.Write(b)
h := sha.Sum(nil)
sha.Reset()
sha.Write(h)
v.estack.PushVal(sha.Sum(nil))
case Ochecksig:
//pubkey := v.estack.Pop().Bytes()
//sig := v.estack.Pop().Bytes()
case Ocheckmultisig:
case Onop:
// unlucky ^^
case Othrow:
panic("THROW")
case Othrowifnot:
if !v.estack.Pop().Bool() {
panic("THROWIFNOT")
}
default:
panic(fmt.Sprintf("unknown opcode %s", op))
}
}
func init() {
log.SetPrefix("NEO-GO-VM > ")
log.SetFlags(0)
}