diff --git a/VERSION b/VERSION index 93d4c1ef0..e095bebd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.36.0 +0.37.0 \ No newline at end of file diff --git a/cli/main.go b/cli/main.go index 8b4db6d7d..0176108d9 100644 --- a/cli/main.go +++ b/cli/main.go @@ -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) diff --git a/cli/vm/vm.go b/cli/vm/vm.go new file mode 100644 index 000000000..0bb55e1db --- /dev/null +++ b/cli/vm/vm.go @@ -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() +} diff --git a/pkg/core/account_state_test.go b/pkg/core/account_state_test.go index e2e4c4e00..461da4f51 100644 --- a/pkg/core/account_state_test.go +++ b/pkg/core/account_state_test.go @@ -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{ diff --git a/pkg/vm/README.md b/pkg/vm/README.md new file mode 100644 index 000000000..39c1d5feb --- /dev/null +++ b/pkg/vm/README.md @@ -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.. diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go new file mode 100644 index 000000000..3f6560e27 --- /dev/null +++ b/pkg/vm/cli/cli.go @@ -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() +} diff --git a/pkg/vm/context.go b/pkg/vm/context.go new file mode 100644 index 000000000..dec801cdc --- /dev/null +++ b/pkg/vm/context.go @@ -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)) +} diff --git a/pkg/vm/interop.go b/pkg/vm/interop.go new file mode 100644 index 000000000..9da8714c5 --- /dev/null +++ b/pkg/vm/interop.go @@ -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) +} diff --git a/pkg/vm/opcode_test.go b/pkg/vm/opcode_test.go new file mode 100644 index 000000000..8c47250ce --- /dev/null +++ b/pkg/vm/opcode_test.go @@ -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 +} diff --git a/pkg/vm/output.go b/pkg/vm/output.go new file mode 100644 index 000000000..119c96104 --- /dev/null +++ b/pkg/vm/output.go @@ -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) +} diff --git a/pkg/vm/script_builder.go b/pkg/vm/script_builder.go index 3d05cb30f..e18c58ca9 100644 --- a/pkg/vm/script_builder.go +++ b/pkg/vm/script_builder.go @@ -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 diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go new file mode 100644 index 000000000..0bf0ab944 --- /dev/null +++ b/pkg/vm/stack.go @@ -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) + } +} diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go new file mode 100644 index 000000000..162c1b449 --- /dev/null +++ b/pkg/vm/stack_item.go @@ -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" +} diff --git a/pkg/vm/stack_test.go b/pkg/vm/stack_test.go new file mode 100644 index 000000000..6576faf26 --- /dev/null +++ b/pkg/vm/stack_test.go @@ -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 +} diff --git a/pkg/vm/state.go b/pkg/vm/state.go new file mode 100644 index 000000000..3ec884b68 --- /dev/null +++ b/pkg/vm/state.go @@ -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" + } +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go new file mode 100644 index 000000000..112095e20 --- /dev/null +++ b/pkg/vm/vm.go @@ -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) +}