From f7d57e4e493ae5be184173ff3383d09e942f20b0 Mon Sep 17 00:00:00 2001 From: Anthony De Meulemeester Date: Fri, 9 Feb 2018 17:08:50 +0100 Subject: [PATCH] VM draft + cli setup (#20) * updated readme * added basic cmd. * added seperate folders for cmd packages. * Fix netmodes in test + reverse bigint bytes * glide get deps --- README.md | 3 +- VERSION | 2 +- cli/main.go | 42 ++---- cli/server/server.go | 56 ++++++++ cli/smartcontract/smart_contract.go | 44 +++++++ glide.lock | 6 +- glide.yaml | 8 +- pkg/network/message.go | 8 +- pkg/network/server.go | 2 +- pkg/network/server_test.go | 6 +- pkg/util/uint256.go | 5 +- pkg/vm/.keep | 0 pkg/vm/compiler.go | 194 ++++++++++++++++++++++++++++ pkg/vm/compiler_test.go | 86 ++++++++++++ pkg/vm/opcode.go | 129 ++++++++++++++++++ pkg/vm/opcode_string.go | 118 +++++++++++++++++ pkg/vm/script_builder.go | 127 ++++++++++++++++++ pkg/vm/script_builder_test.go | 74 +++++++++++ 18 files changed, 861 insertions(+), 49 deletions(-) create mode 100644 cli/server/server.go create mode 100644 cli/smartcontract/smart_contract.go delete mode 100644 pkg/vm/.keep create mode 100644 pkg/vm/compiler.go create mode 100644 pkg/vm/compiler_test.go create mode 100644 pkg/vm/opcode.go create mode 100644 pkg/vm/opcode_string.go create mode 100644 pkg/vm/script_builder.go create mode 100644 pkg/vm/script_builder_test.go diff --git a/README.md b/README.md index b19a5ce7c..e25fd85ed 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,9 @@ The project will exist out of the following packages: | api | started | [@anthdm](https://github.com/anthdm) | | core | started | [@anthdm](https://github.com/anthdm) | | network | started | [@anthdm](https://github.com/anthdm) | +| vm | started | [@anthdm](https://github.com/anthdm) | | smartcontract | started | [@revett](https://github.com/revett) | -| vm | started | [@revett](https://github.com/revett) | +| cli | started | [@revett](https://github.com/revett) | # Getting Started diff --git a/VERSION b/VERSION index a3df0a695..ac39a106c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.0 +0.9.0 diff --git a/cli/main.go b/cli/main.go index c8b13fd1a..2a5fb679f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,43 +1,21 @@ package main import ( - "flag" - "strings" + "os" - "github.com/CityOfZion/neo-go/pkg/network" + "github.com/CityOfZion/neo-go/cli/server" + "github.com/CityOfZion/neo-go/cli/smartcontract" + "github.com/urfave/cli" ) -var ( - tcp = flag.Int("tcp", 3000, "port TCP listener will listen on.") - seed = flag.String("seed", "", "initial seed servers.") - net = flag.Int("net", 56753, "the mode the server will operate in.") - rpc = flag.Int("rpc", 0, "let this server also respond to rpc calls on this port") -) - -// Simple dirty and quick bootstrapping for the sake of development. -// e.g run 2 nodes: -// neoserver -tcp :4000 -// neoserver -tcp :3000 -seed 127.0.0.1:4000 func main() { - flag.Parse() + ctl := cli.NewApp() + ctl.Name = "neo-go" - opts := network.StartOpts{ - Seeds: parseSeeds(*seed), - TCP: *tcp, - RPC: *rpc, + ctl.Commands = []cli.Command{ + server.NewCommand(), + smartcontract.NewCommand(), } - s := network.NewServer(network.NetMode(*net)) - s.Start(opts) -} - -func parseSeeds(s string) []string { - if len(s) == 0 { - return nil - } - seeds := strings.Split(s, ",") - if len(seeds) == 0 { - return nil - } - return seeds + ctl.Run(os.Args) } diff --git a/cli/server/server.go b/cli/server/server.go new file mode 100644 index 000000000..a13681b72 --- /dev/null +++ b/cli/server/server.go @@ -0,0 +1,56 @@ +package server + +import ( + "strings" + + "github.com/CityOfZion/neo-go/pkg/network" + "github.com/urfave/cli" +) + +// NewCommand creates a new Node command +func NewCommand() cli.Command { + return cli.Command{ + Name: "node", + Usage: "start a NEO node", + Action: startServer, + Flags: []cli.Flag{ + cli.IntFlag{Name: "tcp"}, + cli.IntFlag{Name: "rpc"}, + cli.StringFlag{Name: "seed"}, + cli.BoolFlag{Name: "privnet, p"}, + cli.BoolFlag{Name: "mainnet, m"}, + cli.BoolFlag{Name: "testnet, t"}, + }, + } +} + +func startServer(ctx *cli.Context) error { + opts := network.StartOpts{ + Seeds: parseSeeds(ctx.String("seed")), + TCP: ctx.Int("tcp"), + RPC: ctx.Int("rpc"), + } + + net := network.ModePrivNet + if ctx.Bool("testnet") { + net = network.ModeTestNet + } + if ctx.Bool("mainnet") { + net = network.ModeMainNet + } + + s := network.NewServer(net) + s.Start(opts) + return nil +} + +func parseSeeds(s string) []string { + if len(s) == 0 { + return nil + } + seeds := strings.Split(s, ",") + if len(seeds) == 0 { + return nil + } + return seeds +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go new file mode 100644 index 000000000..2f2b00b9f --- /dev/null +++ b/cli/smartcontract/smart_contract.go @@ -0,0 +1,44 @@ +package smartcontract + +import ( + "fmt" + + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/urfave/cli" +) + +// NewCommand returns a new contract command. +func NewCommand() cli.Command { + return cli.Command{ + Name: "contract", + Usage: "compile - debug - deploy smart contracts", + Subcommands: []cli.Command{ + { + Name: "compile", + Usage: "compile a smart contract to a .avm file", + Action: contractCompile, + }, + { + Name: "opdump", + Usage: "dump the opcode of a .go file", + Action: contractDumpOpcode, + }, + }, + } +} + +func contractCompile(ctx *cli.Context) error { + fmt.Println("compile") + return nil +} + +func contractDumpOpcode(ctx *cli.Context) error { + src := ctx.Args()[0] + + c := vm.NewCompiler() + if err := c.CompileSource(src); err != nil { + return err + } + c.DumpOpcode() + return nil +} diff --git a/glide.lock b/glide.lock index 5cc8c47c4..fa0b5bca1 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 2b8debf7936e789545da367433ddbf5f0e3bb54658340d6a412970bca25e6335 -updated: 2018-02-05T16:11:44.616743+01:00 +hash: 5cb2fb077aa0e57c788a99f9ded4bf2d421a04f12f6b8436234d12e93f418f36 +updated: 2018-02-09T16:49:32.538502496+01:00 imports: - name: github.com/golang/snappy version: 553a641470496b2327abcac10b36396bd98e45c9 @@ -18,4 +18,6 @@ imports: - leveldb/storage - leveldb/table - leveldb/util +- name: github.com/urfave/cli + version: cfb38830724cc34fedffe9a2a29fb54fa9169cd1 testImports: [] diff --git a/glide.yaml b/glide.yaml index 5e77a1723..226a13a2b 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,5 +1,7 @@ package: github.com/CityOfZion/neo-go import: - - package: github.com/syndtr/goleveldb/leveldb - - +- package: github.com/syndtr/goleveldb + subpackages: + - leveldb +- package: github.com/urfave/cli + version: ^1.20.0 diff --git a/pkg/network/message.go b/pkg/network/message.go index 5e1c8ea92..06fb6e87c 100644 --- a/pkg/network/message.go +++ b/pkg/network/message.go @@ -28,8 +28,8 @@ type NetMode uint32 // String implements the stringer interface. func (n NetMode) String() string { switch n { - case ModeDevNet: - return "devnet" + case ModePrivNet: + return "privnet" case ModeTestNet: return "testnet" case ModeMainNet: @@ -42,8 +42,8 @@ func (n NetMode) String() string { // Values used for the magic field, according to the docs. const ( ModeMainNet NetMode = 0x00746e41 // 7630401 - ModeTestNet = 0x74746e41 // 1953787457 - ModeDevNet = 56753 // docker privnet + ModeTestNet NetMode = 0x74746e41 // 1953787457 + ModePrivNet NetMode = 56753 // docker privnet ) // Message is the complete message send between nodes. diff --git a/pkg/network/server.go b/pkg/network/server.go index cb6a7d1a4..66b7303ab 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -105,7 +105,7 @@ func (m protectedHashmap) has(h util.Uint256) bool { func NewServer(net NetMode) *Server { logger := log.New(os.Stdout, "[NEO SERVER] :: ", 0) - if net != ModeTestNet && net != ModeMainNet && net != ModeDevNet { + if net != ModeTestNet && net != ModeMainNet && net != ModePrivNet { logger.Fatalf("invalid network mode %d", net) } diff --git a/pkg/network/server_test.go b/pkg/network/server_test.go index d072ba6c5..59a9cd0f3 100644 --- a/pkg/network/server_test.go +++ b/pkg/network/server_test.go @@ -9,7 +9,7 @@ import ( // TODO this should be moved to localPeer test. func TestHandleVersionFailWrongPort(t *testing.T) { - s := NewServer(ModeDevNet) + s := NewServer(ModePrivNet) go s.loop() p := NewLocalPeer(s) @@ -21,7 +21,7 @@ func TestHandleVersionFailWrongPort(t *testing.T) { } func TestHandleVersionFailIdenticalNonce(t *testing.T) { - s := NewServer(ModeDevNet) + s := NewServer(ModePrivNet) go s.loop() p := NewLocalPeer(s) @@ -33,7 +33,7 @@ func TestHandleVersionFailIdenticalNonce(t *testing.T) { } func TestHandleVersion(t *testing.T) { - s := NewServer(ModeDevNet) + s := NewServer(ModePrivNet) go s.loop() p := NewLocalPeer(s) diff --git a/pkg/util/uint256.go b/pkg/util/uint256.go index 9ad7537eb..ee2a6ec54 100644 --- a/pkg/util/uint256.go +++ b/pkg/util/uint256.go @@ -23,7 +23,7 @@ func Uint256DecodeFromString(s string) (Uint256, error) { return val, err } - b = ReverseByteSlice(b) + b = ToArrayReverse(b) return Uint256DecodeFromBytes(b) } @@ -43,7 +43,8 @@ func Uint256DecodeFromBytes(b []byte) (Uint256, error) { return val, nil } -func ReverseByteSlice(b []byte) []byte { +// ToArrayReverse return a reversed version of the given byte slice. +func ToArrayReverse(b []byte) []byte { dest := make([]byte, len(b)) for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { diff --git a/pkg/vm/.keep b/pkg/vm/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkg/vm/compiler.go b/pkg/vm/compiler.go new file mode 100644 index 000000000..9aeb7282d --- /dev/null +++ b/pkg/vm/compiler.go @@ -0,0 +1,194 @@ +package vm + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "os" + "reflect" + "strconv" + "strings" +) + +const ( + outputExt = ".avm" +) + +// Syscalls that have no return value. +var nonReturnSysCalls = []string{ + "Notify", "print", "Log", "Put", "Register", + "append", "Delete", "SetVotes", "ContractDestroy", + "MerkleRoot", "Hash", "PrevHash", "GetHeader"} + +// Compiler holds the output buffer of the compiled source. +type Compiler struct { + // Output extension of the file. Default .avm. + OutputExt string + sb *ScriptBuilder + curLineNum int + + i int + varList []Variable + vars map[string]Variable +} + +// NewCompiler returns a new compiler ready to compile smartcontracts. +func NewCompiler() *Compiler { + return &Compiler{ + OutputExt: outputExt, + sb: &ScriptBuilder{new(bytes.Buffer)}, + vars: map[string]Variable{}, + varList: []Variable{}, + } +} + +// CompileSource will compile the source file into an avm format. +func (c *Compiler) CompileSource(src string) error { + file, err := os.Open(src) + if err != nil { + return err + } + return c.Compile(file) +} + +// Visit implements the ast.Visitor interface. +func (c *Compiler) Visit(node ast.Node) ast.Visitor { + switch t := node.(type) { + case *ast.AssignStmt: + c.processAssignStmt(t) + case *ast.FuncDecl: + case *ast.ReturnStmt: + } + return c +} + +func (c *Compiler) newVariable(k token.Token, i, val string) Variable { + v := Variable{ + kind: k, + ident: i, + value: val, + pos: c.i, + } + c.vars[v.ident] = v + c.i++ + return v +} + +func (c *Compiler) initialize(n OpCode) { + // Get the (n) localVars which is basicly the number of args passed in Main + // and the number of local Vars in the function body. + c.sb.emitPush(n) + c.sb.emitPush(OpNewArray) + c.sb.emitPush(OpToAltStack) +} + +func (c *Compiler) teardown() { + c.sb.emitPush(OpNOP) + c.sb.emitPush(OpFromAltStack) + c.sb.emitPush(OpDrop) + c.sb.emitPush(OpRET) +} + +// Push a variable on to the stack. +func (c *Compiler) storeLocal(v Variable) { + if v.kind == token.INT { + val, _ := strconv.Atoi(v.value) + c.sb.emitPushInt(int64(val)) + } + if v.kind == token.STRING { + val := strings.Replace(v.value, `"`, "", 2) + c.sb.emitPushString(val) + } + + c.sb.emitPush(OpFromAltStack) + c.sb.emitPush(OpDup) + c.sb.emitPush(OpToAltStack) + + pos := int64(v.pos) + + c.sb.emitPushInt(pos) + c.sb.emitPushInt(2) + c.sb.emitPush(OpRoll) + c.sb.emitPush(OpSetItem) +} + +func (c *Compiler) loadLocal(ident string) { + val, ok := c.vars[ident] + if !ok { + c.reportError(fmt.Sprintf("local variable %s not found", ident)) + } + + pos := int64(val.pos) + + c.sb.emitPush(OpFromAltStack) + c.sb.emitPush(OpDup) + c.sb.emitPush(OpToAltStack) + + // push it's index on the stack + c.sb.emitPushInt(pos) + c.sb.emitPush(OpPickItem) +} + +// TODO: instead of passing the stmt in to this, put the lhs and rhs in. +// so we can reuse this. +func (c *Compiler) processAssignStmt(stmt *ast.AssignStmt) { + lhs := stmt.Lhs[0].(*ast.Ident) + switch t := stmt.Rhs[0].(type) { + case *ast.BasicLit: + c.storeLocal(c.newVariable(t.Kind, lhs.Name, t.Value)) + case *ast.CompositeLit: + switch t.Type.(type) { + case *ast.StructType: + c.reportError("assigning struct literals not yet implemented") + case *ast.ArrayType: + // for _, expr := range t.Elts { + // v := expr.(*ast.BasicLit) + // c.storeLocal(c.newVariable(v.Kind, lhs.Name, v.Value)) + // } + } + case *ast.Ident: + c.loadLocal(t.Name) + case *ast.FuncLit: + c.reportError("assigning function literals not yet implemented") + default: + fmt.Println(reflect.TypeOf(t)) + } +} + +// Compile will compile from r into an avm format. +func (c *Compiler) Compile(r io.Reader) error { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", r, 0) + if err != nil { + return err + } + + c.initialize(OpPush2) // initialize the compiler with n local stack vars. + ast.Walk(c, f) // walk through and process the AST + c.teardown() // done compiling + + return nil +} + +// TODO: More detailed report (lineno, ...) +func (c *Compiler) reportError(msg string) { + fmt.Printf("COMPILER ERROR :: %s\n", msg) + os.Exit(1) +} + +// DumpOpcode dumps the current buffer, formatted with index, hex and opcode. +// Usefull for debugging smartcontracts. +func (c *Compiler) DumpOpcode() { + c.sb.dumpOpcode() +} + +// A Variable can represent any variable in the program. +type Variable struct { + ident string + kind token.Token + value string + pos int +} diff --git a/pkg/vm/compiler_test.go b/pkg/vm/compiler_test.go new file mode 100644 index 000000000..52017aaf2 --- /dev/null +++ b/pkg/vm/compiler_test.go @@ -0,0 +1,86 @@ +package vm + +import ( + "strings" + "testing" +) + +// opener +// +// 0x53 push len arguments +// 0xc5 open new array +// 0x6B to alt stack +// +// 0x5A the number 10 is pushed on the stack +// 0x6C put the input onto the main stack remove from alt +// 0x76 dup the item on top of the stack +// 0x6B put the item on the alt stack +// 0x00 put empty array on the stack +// 0x52 put the number 2 on the stack +// 0x7A put the item n back on top of the stack +// 0xC4 set item +// 0x59 put the number 9 on the stack +// 0x6C put the input onto the main stack remove from alt stack +// 0x76 dup the item on top of the stackj +// 0x6B put the item on the alt stack +// 0x51 push the number 1 on the stack +// 0x52 push the number 2 on the stack +// 0x7A put the item n back on top of the stack +// 0xC4 set the item +// 0x62 JMP +// 0x03 the next 3 bytes is dat pushed on the stack +// 0x6C put the input ont the main stack remove from alt stack +// 0x00 put empty array onto the stack +// 0x02 the next 2 bytes is data pushed on the stack +// 0xE8 1000 uint16 +// 0x03 1000 uint16 +// 0x6C put the input onto the main stack remove from alt +// 0x76 dup the item on top of the stack +// 0x6B put the item on the alt stack +// 0x52 push the number 2 on the stack +// 0x52 push the number 2 on the stack +// 0x7A put the item n back on top of the stack +// 0xC4 set the item +// 0x00 empty array is pushed on the stack +// 0x61 nop +// 0x6C put the input onto the main stack remove from alt +// 0x75 removes the top stack item +// 0x66 return + +// + +func TestSimpleAssign(t *testing.T) { + src := ` + package NEP5 + + func Main() { + x := 10 + y := 8 + } + ` + + c := NewCompiler() + if err := c.Compile(strings.NewReader(src)); err != nil { + t.Fatal(err) + } + + // c.DumpOpcode() +} + +func TestAssignLoadLocal(t *testing.T) { + src := ` + package NEP5 + + func Main() { + x := 1 + y := x + } + ` + + c := NewCompiler() + if err := c.Compile(strings.NewReader(src)); err != nil { + t.Fatal(err) + } + + // c.DumpOpcode() +} diff --git a/pkg/vm/opcode.go b/pkg/vm/opcode.go new file mode 100644 index 000000000..cf36bc059 --- /dev/null +++ b/pkg/vm/opcode.go @@ -0,0 +1,129 @@ +package vm + +// OpCode is an single operational instruction for the GO NEO virtual machine. +type OpCode byte + +// List of supported opcodes. +const ( + // Constants + OpPush0 OpCode = 0x00 // An empty array of bytes is pushed onto the stack. + OpPushF OpCode = OpPush0 + OpPushBytes1 OpCode = 0x01 // 0x01-0x4B The next opcode bytes is data to be pushed onto the stack + OpPushBytes75 OpCode = 0x4B + OpPushData1 OpCode = 0x4C // The next byte contains the number of bytes to be pushed onto the stack. + OpPushData2 OpCode = 0x4D // The next two bytes contain the number of bytes to be pushed onto the stack. + OpPushData4 OpCode = 0x4E // The next four bytes contain the number of bytes to be pushed onto the stack. + OpPushM1 OpCode = 0x4F // The number -1 is pushed onto the stack. + OpPush1 OpCode = 0x51 + OpPushT OpCode = OpPush1 + OpPush2 OpCode = 0x52 // The number 2 is pushed onto the stack. + OpPush3 OpCode = 0x53 // The number 3 is pushed onto the stack. + OpPush4 OpCode = 0x54 // The number 4 is pushed onto the stack. + OpPush5 OpCode = 0x55 // The number 5 is pushed onto the stack. + OpPush6 OpCode = 0x56 // The number 6 is pushed onto the stack. + OpPush7 OpCode = 0x57 // The number 7 is pushed onto the stack. + OpPush8 OpCode = 0x58 // The number 8 is pushed onto the stack. + OpPush9 OpCode = 0x59 // The number 9 is pushed onto the stack. + OpPush10 OpCode = 0x5A // The number 10 is pushed onto the stack. + OpPush11 OpCode = 0x5B // The number 11 is pushed onto the stack. + OpPush12 OpCode = 0x5C // The number 12 is pushed onto the stack. + OpPush13 OpCode = 0x5D // The number 13 is pushed onto the stack. + OpPush14 OpCode = 0x5E // The number 14 is pushed onto the stack. + OpPush15 OpCode = 0x5F // The number 15 is pushed onto the stack. + OpPush16 OpCode = 0x60 // The number 16 is pushed onto the stack. + + // Flow control + OpNOP OpCode = 0x61 // No operation. + OpJMP OpCode = 0x62 + OpJMPIF OpCode = 0x63 + OpJMPIFNOT OpCode = 0x64 + OpCall OpCode = 0x65 + OpRET OpCode = 0x66 + OpAppCall OpCode = 0x67 + OpSysCall OpCode = 0x68 + OpTailCall OpCode = 0x69 + + // The stack + OpDupFromAltStack OpCode = 0x6A + OpToAltStack OpCode = 0x6B // Puts the input onto the top of the alt stack. Removes it from the main stack. + OpFromAltStack OpCode = 0x6C // Puts the input onto the top of the main stack. Removes it from the alt stack. + OpXDrop OpCode = 0x6D + OpXSwap OpCode = 0x72 + OpXTuck OpCode = 0x73 + OpDepth OpCode = 0x74 // Puts the number of stack items onto the stack. + OpDrop OpCode = 0x75 // Removes the top stack item. + OpDup OpCode = 0x76 // Duplicates the top stack item. + OpNip OpCode = 0x77 // Removes the second-to-top stack item. + OpOver OpCode = 0x78 // Copies the second-to-top stack item to the top. + OpPick OpCode = 0x79 // The item n back in the stack is copied to the top. + OpRoll OpCode = 0x7A // The item n back in the stack is moved to the top. + OpRot OpCode = 0x7B // The top three items on the stack are rotated to the left. + OpSwap OpCode = 0x7C // The top two items on the stack are swapped. + OpTuck OpCode = 0x7D // The item at the top of the stack is copied and inserted before the second-to-top item. + + // Splice + OpCat OpCode = 0x7E // Concatenates two strings. + OpSubStr OpCode = 0x7F // Returns a section of a string. + OpLeft OpCode = 0x80 // Keeps only characters left of the specified point in a string. + OpRight OpCode = 0x81 // Keeps only characters right of the specified point in a string. + OpSize OpCode = 0x82 // Returns the length of the input string. + + // Bitwise logic + OpInvert OpCode = 0x83 // Flips all of the bits in the input. + OpAnd OpCode = 0x84 // Boolean and between each bit in the inputs. + OpOr OpCode = 0x85 // Boolean or between each bit in the inputs. + OpXor OpCode = 0x86 // Boolean exclusive or between each bit in the inputs. + OpEqual OpCode = 0x87 // Returns 1 if the inputs are exactly equal, 0 otherwise. + + // Arithmetic + // Note: Arithmetic inputs are limited to signed 32-bit integers, but may overflow their output. + OpInc OpCode = 0x8B // 1 is added to the input. + OpDec OpCode = 0x8C // 1 is subtracted from the input. + OpSign OpCode = 0x8D + OpNegate OpCode = 0x8F // The sign of the input is flipped. + OpAbs OpCode = 0x90 // The input is made positive. + OpNot OpCode = 0x91 // If the input is 0 or 1, it is flipped. Otherwise the output will be 0. + OpNZ OpCode = 0x92 // Returns 0 if the input is 0. 1 otherwise. + OpAdd OpCode = 0x93 // a is added to b. + OpSub OpCode = 0x94 // b is subtracted from a. + OpMul OpCode = 0x95 // a is multiplied by b. + OpDiv OpCode = 0x96 // a is divided by b. + OpMod OpCode = 0x97 // Returns the remainder after dividing a by b. + OpShl OpCode = 0x98 // Shifts a left b bits, preserving sign. + OpShr OpCode = 0x99 // Shifts a right b bits, preserving sign. + OpBoolAnd OpCode = 0x9A // If both a and b are not 0, the output is 1. Otherwise 0. + OpBoolOr OpCode = 0x9B // If a or b is not 0, the output is 1. Otherwise 0. + OpNumEqual OpCode = 0x9C // Returns 1 if the numbers are equal, 0 otherwise. + OpNumNotEqual OpCode = 0x9E // Returns 1 if the numbers are not equal, 0 otherwise. + OpLT OpCode = 0x9F // Returns 1 if a is less than b, 0 otherwise. + OpGT OpCode = 0xA0 // Returns 1 if a is greater than b, 0 otherwise. + OpLTE OpCode = 0xA1 // Returns 1 if a is less than or equal to b, 0 otherwise. + OpGTE OpCode = 0xA2 // Returns 1 if a is greater than or equal to b, 0 otherwise. + OpMin OpCode = 0xA3 // Returns the smaller of a and b. + OpMax OpCode = 0xA4 // Returns the larger of a and b. + OpWithin OpCode = 0xA5 // Returns 1 if x is within the specified range (left-inclusive), 0 otherwise. + + // Crypto + OpSHA1 OpCode = 0xA7 // The input is hashed using SHA-1. + OpSHA256 OpCode = 0xA8 // The input is hashed using SHA-256. + OpHASH160 OpCode = 0xA9 + OpHASH256 OpCode = 0xAA + OpCheckSig OpCode = 0xAC + OpCheckMultiSig OpCode = 0xAE + + // Array + OpArraySize OpCode = 0xC0 + OpPack OpCode = 0xC1 + OpUnpack OpCode = 0xC2 + OpPickItem OpCode = 0xC3 + OpSetItem OpCode = 0xC4 + OpNewArray OpCode = 0xC5 // Pops size from stack and creates a new array with that size, and pushes the array into the stack + OpNewStruct OpCode = 0xC6 + OpAppend OpCode = 0xC8 + OpReverse OpCode = 0xC9 + OpRemove OpCode = 0xCA + + // Exceptions + OpThrow OpCode = 0xF0 + OpThrowIfNot OpCode = 0xF1 +) diff --git a/pkg/vm/opcode_string.go b/pkg/vm/opcode_string.go new file mode 100644 index 000000000..67e922e35 --- /dev/null +++ b/pkg/vm/opcode_string.go @@ -0,0 +1,118 @@ +// Code generated by "stringer -type=OpCode ./pkg/vm"; DO NOT EDIT. + +package vm + +import "strconv" + +const _OpCode_name = "OpPush0OpPushBytes1OpPushBytes75OpPushData1OpPushData2OpPushData4OpPushM1OpPush1OpPush2OpPush3OpPush4OpPush5OpPush6OpPush7OpPush8OpPush9OpPush10OpPush11OpPush12OpPush13OpPush14OpPush15OpPush16OpNOPOpJMPOpJMPIFOpJMPIFNOTOpCallOpRETOpAppCallOpSysCallOpTailCallOpDupFromAltStackOpToAltStackOpFromAltStackOpXDropOpXSwapOpXTuckOpDepthOpDropOpDupOpNipOpOverOpPickOpRollOpRotOpSwapOpTuckOpCatOpSubStrOpLeftOpRightOpSizeOpInvertOpAndOpOrOpXorOpEqualOpIncOpDecOpSignOpNegateOpAbsOpNotOpNZOpAddOpSubOpMulOpDivOpModOpShlOpShrOpBoolAndOpBoolOrOpNumEqualOpNumNotEqualOpLTOpGTOpLTEOpGTEOpMinOpMaxOpWithinOpSHA1OpSHA256OpHASH160OpHASH256OpCheckSigOpCheckMultiSigOpArraySizeOpPackOpUnpackOpPickItemOpSetItemOpNewArrayOpNewStructOpAppendOpReverseOpRemoveOpThrowOpThrowIfNot" + +var _OpCode_map = map[OpCode]string{ + 0: _OpCode_name[0:7], + 1: _OpCode_name[7:19], + 75: _OpCode_name[19:32], + 76: _OpCode_name[32:43], + 77: _OpCode_name[43:54], + 78: _OpCode_name[54:65], + 79: _OpCode_name[65:73], + 81: _OpCode_name[73:80], + 82: _OpCode_name[80:87], + 83: _OpCode_name[87:94], + 84: _OpCode_name[94:101], + 85: _OpCode_name[101:108], + 86: _OpCode_name[108:115], + 87: _OpCode_name[115:122], + 88: _OpCode_name[122:129], + 89: _OpCode_name[129:136], + 90: _OpCode_name[136:144], + 91: _OpCode_name[144:152], + 92: _OpCode_name[152:160], + 93: _OpCode_name[160:168], + 94: _OpCode_name[168:176], + 95: _OpCode_name[176:184], + 96: _OpCode_name[184:192], + 97: _OpCode_name[192:197], + 98: _OpCode_name[197:202], + 99: _OpCode_name[202:209], + 100: _OpCode_name[209:219], + 101: _OpCode_name[219:225], + 102: _OpCode_name[225:230], + 103: _OpCode_name[230:239], + 104: _OpCode_name[239:248], + 105: _OpCode_name[248:258], + 106: _OpCode_name[258:275], + 107: _OpCode_name[275:287], + 108: _OpCode_name[287:301], + 109: _OpCode_name[301:308], + 114: _OpCode_name[308:315], + 115: _OpCode_name[315:322], + 116: _OpCode_name[322:329], + 117: _OpCode_name[329:335], + 118: _OpCode_name[335:340], + 119: _OpCode_name[340:345], + 120: _OpCode_name[345:351], + 121: _OpCode_name[351:357], + 122: _OpCode_name[357:363], + 123: _OpCode_name[363:368], + 124: _OpCode_name[368:374], + 125: _OpCode_name[374:380], + 126: _OpCode_name[380:385], + 127: _OpCode_name[385:393], + 128: _OpCode_name[393:399], + 129: _OpCode_name[399:406], + 130: _OpCode_name[406:412], + 131: _OpCode_name[412:420], + 132: _OpCode_name[420:425], + 133: _OpCode_name[425:429], + 134: _OpCode_name[429:434], + 135: _OpCode_name[434:441], + 139: _OpCode_name[441:446], + 140: _OpCode_name[446:451], + 141: _OpCode_name[451:457], + 143: _OpCode_name[457:465], + 144: _OpCode_name[465:470], + 145: _OpCode_name[470:475], + 146: _OpCode_name[475:479], + 147: _OpCode_name[479:484], + 148: _OpCode_name[484:489], + 149: _OpCode_name[489:494], + 150: _OpCode_name[494:499], + 151: _OpCode_name[499:504], + 152: _OpCode_name[504:509], + 153: _OpCode_name[509:514], + 154: _OpCode_name[514:523], + 155: _OpCode_name[523:531], + 156: _OpCode_name[531:541], + 158: _OpCode_name[541:554], + 159: _OpCode_name[554:558], + 160: _OpCode_name[558:562], + 161: _OpCode_name[562:567], + 162: _OpCode_name[567:572], + 163: _OpCode_name[572:577], + 164: _OpCode_name[577:582], + 165: _OpCode_name[582:590], + 167: _OpCode_name[590:596], + 168: _OpCode_name[596:604], + 169: _OpCode_name[604:613], + 170: _OpCode_name[613:622], + 172: _OpCode_name[622:632], + 174: _OpCode_name[632:647], + 192: _OpCode_name[647:658], + 193: _OpCode_name[658:664], + 194: _OpCode_name[664:672], + 195: _OpCode_name[672:682], + 196: _OpCode_name[682:691], + 197: _OpCode_name[691:701], + 198: _OpCode_name[701:712], + 200: _OpCode_name[712:720], + 201: _OpCode_name[720:729], + 202: _OpCode_name[729:737], + 240: _OpCode_name[737:744], + 241: _OpCode_name[744:756], +} + +func (i OpCode) String() string { + if str, ok := _OpCode_map[i]; ok { + return str + } + return "OpCode(" + strconv.FormatInt(int64(i), 10) + ")" +} diff --git a/pkg/vm/script_builder.go b/pkg/vm/script_builder.go new file mode 100644 index 000000000..19644f6f6 --- /dev/null +++ b/pkg/vm/script_builder.go @@ -0,0 +1,127 @@ +package vm + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/CityOfZion/neo-go/pkg/util" +) + +// ScriptBuilder generates bytecode and will write all +// generated bytecode into its internal buffer. +type ScriptBuilder struct { + buf *bytes.Buffer +} + +func (sb *ScriptBuilder) emit(op OpCode, b []byte) error { + if err := sb.buf.WriteByte(byte(op)); err != nil { + return err + } + _, err := sb.buf.Write(b) + return err +} + +func (sb *ScriptBuilder) emitPush(op OpCode) error { + return sb.buf.WriteByte(byte(op)) +} + +func (sb *ScriptBuilder) emitPushBool(b bool) error { + if b { + return sb.emitPush(OpPushT) + } + return sb.emitPush(OpPushF) +} + +func (sb *ScriptBuilder) emitPushInt(i int64) error { + if i == -1 { + return sb.emitPush(OpPushM1) + } + if i == 0 { + return sb.emitPush(OpPushF) + } + if i > 0 && i < 16 { + val := OpCode((int(OpPush1) - 1 + int(i))) + return sb.emitPush(val) + } + + bInt := big.NewInt(i) + val := util.ToArrayReverse(bInt.Bytes()) + return sb.emitPushArray(val) +} + +func (sb *ScriptBuilder) emitPushArray(b []byte) error { + var ( + err error + n = len(b) + ) + + if n == 0 { + return errors.New("0 bytes given in pushArray") + } + if n <= int(OpPushBytes75) { + return sb.emit(OpCode(n), b) + } else if n < 0x100 { + err = sb.emit(OpPushData1, []byte{byte(n)}) + } else if n < 0x10000 { + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(n)) + err = sb.emit(OpPushData2, buf) + } else { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(n)) + err = sb.emit(OpPushData4, buf) + } + if err != nil { + return err + } + _, err = sb.buf.Write(b) + return err +} + +func (sb *ScriptBuilder) emitPushString(str string) error { + return sb.emitPushArray([]byte(str)) +} + +func (sb *ScriptBuilder) emitSysCall(api string) error { + lenAPI := len(api) + if lenAPI == 0 { + return errors.New("syscall argument cant be 0") + } + if lenAPI > 252 { + return fmt.Errorf("invalid syscall argument: %s", api) + } + + bapi := []byte(api) + args := make([]byte, lenAPI+1) + args[0] = byte(lenAPI) + copy(args, bapi[1:]) + return sb.emit(OpSysCall, args) +} + +func (sb *ScriptBuilder) emitPushCall(scriptHash []byte, tailCall bool) error { + if len(scriptHash) != 20 { + return errors.New("expected a 20 byte long scriptHash (uint160) for pushCall") + } + op := OpAppCall + if tailCall { + op = OpTailCall + } + return sb.emit(op, scriptHash) +} + +func (sb *ScriptBuilder) emitJump(op OpCode, offset int16) error { + if op != OpJMP && op != OpJMPIF && op != OpJMPIFNOT && op != OpCall { + return fmt.Errorf("invalid jump opcode: %v", op) + } + return sb.emit(op, []byte{}) // convert to bits? +} + +func (sb *ScriptBuilder) dumpOpcode() { + buf := sb.buf.Bytes() + for i := 0; i < len(buf); i++ { + fmt.Printf("OPCODE AT INDEX \t %d \t 0x%2x \t %s \n", i, buf[i], OpCode(buf[i])) + } +} diff --git a/pkg/vm/script_builder_test.go b/pkg/vm/script_builder_test.go new file mode 100644 index 000000000..5e29aff05 --- /dev/null +++ b/pkg/vm/script_builder_test.go @@ -0,0 +1,74 @@ +package vm + +import ( + "bytes" + "math/big" + "testing" + + "github.com/CityOfZion/neo-go/pkg/util" +) + +func TestEmitPush(t *testing.T) { + sb := &ScriptBuilder{new(bytes.Buffer)} + + if err := sb.emitPush(OpPush1); err != nil { + t.Fatal(err) + } + if sb.buf.Len() != 1 { + t.Fatalf("expect buffer len of 1 got %d", sb.buf.Len()) + } +} + +func TestEmitPushInt(t *testing.T) { + sb := &ScriptBuilder{new(bytes.Buffer)} + + val := -1 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + if want, have := OpPushM1, OpCode(sb.buf.Bytes()[0]); want != have { + t.Fatalf("expected %v got %v", want, have) + } + val = 0 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + if want, have := OpPushF, OpCode(sb.buf.Bytes()[1]); want != have { + t.Fatalf("expected %v got %v", want, have) + } + val = 1 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + if want, have := OpPush1, OpCode(sb.buf.Bytes()[2]); want != have { + t.Fatalf("expected %v got %v", want, have) + } + val = 1000 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + bInt := big.NewInt(int64(val)) + if want, have := byte(len(bInt.Bytes())), byte(sb.buf.Bytes()[3]); want != have { + t.Fatalf("expected %v got %v", want, have) + } + want := util.ToArrayReverse(bInt.Bytes()) // reverse + have := sb.buf.Bytes()[4:] + if bytes.Compare(want, have) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} + +func TestEmitPushString(t *testing.T) { + sb := &ScriptBuilder{new(bytes.Buffer)} + str := "anthdm" + if err := sb.emitPushString(str); err != nil { + t.Fatal(err) + } + if want, have := byte(len(str)), sb.buf.Bytes()[0]; want != have { + t.Fatalf("expected %v got %v", want, have) + } + want, have := []byte(str), sb.buf.Bytes()[1:] + if bytes.Compare(want, have) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +}