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:
parent
0b023c5c5c
commit
931388b687
16 changed files with 1914 additions and 3 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.36.0
|
||||
0.37.0
|
|
@ -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
23
cli/vm/vm.go
Normal 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()
|
||||
}
|
|
@ -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
140
pkg/vm/README.md
Normal 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
168
pkg/vm/cli/cli.go
Normal 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
123
pkg/vm/context.go
Normal 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
32
pkg/vm/interop.go
Normal 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
223
pkg/vm/opcode_test.go
Normal 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
25
pkg/vm/output.go
Normal 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)
|
||||
}
|
|
@ -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
234
pkg/vm/stack.go
Normal 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
113
pkg/vm/stack_item.go
Normal 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
182
pkg/vm/stack_test.go
Normal 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
25
pkg/vm/state.go
Normal 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
619
pkg/vm/vm.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue