diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 86bc1af41..05b3a6004 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -6,18 +6,20 @@ import ( "context" "encoding/hex" "encoding/json" - "errors" "fmt" "io/ioutil" "os" "path/filepath" "github.com/CityOfZion/neo-go/pkg/rpc" + "github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm/compiler" + "github.com/pkg/errors" "github.com/urfave/cli" ) var ( + errNoEndpoint = errors.New("no RPC endpoint specified, use option '--endpoint' or '-e'") errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") errFileExist = errors.New("A file with given smart-contract name already exists") @@ -65,6 +67,10 @@ func NewCommands() []cli.Command { Usage: "Test an invocation of a smart contract on the blockchain", Action: testInvoke, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "endpoint, e", + Usage: "RPC endpoint address (like 'http://seed4.ngd.network:20332')", + }, cli.StringFlag{ Name: "in, i", Usage: "Input location of the avm file that needs to be invoked", @@ -91,6 +97,10 @@ func NewCommands() []cli.Command { Usage: "creates a user readable dump of the program instructions", Action: inspect, Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "compile, c", + Usage: "compile input file (it should be go code then)", + }, cli.StringFlag{ Name: "in, i", Usage: "input file of the program", @@ -163,16 +173,16 @@ func testInvoke(ctx *cli.Context) error { if len(src) == 0 { return cli.NewExitError(errNoInput, 1) } + endpoint := ctx.String("endpoint") + if len(endpoint) == 0 { + return cli.NewExitError(errNoEndpoint, 1) + } b, err := ioutil.ReadFile(src) if err != nil { return cli.NewExitError(err, 1) } - // For now we will hardcode the endpoint. - // On the long term the internal VM will run the script. - // TODO: remove RPC dependency, hardcoded node. - endpoint := "http://node1.ams2.bridgeprotocol.io:10332" client, err := rpc.NewClient(context.TODO(), endpoint, rpc.ClientOptions{}) if err != nil { return cli.NewExitError(err, 1) @@ -251,12 +261,24 @@ func parseContractDetails() ContractDetails { } func inspect(ctx *cli.Context) error { - src := ctx.String("in") - if len(src) == 0 { + in := ctx.String("in") + compile := ctx.Bool("compile") + if len(in) == 0 { return cli.NewExitError(errNoInput, 1) } - if err := compiler.CompileAndInspect(src); err != nil { + b, err := ioutil.ReadFile(in) + if err != nil { return cli.NewExitError(err, 1) } + if compile { + b, err = compiler.Compile(bytes.NewReader(b)) + if err != nil { + return cli.NewExitError(errors.Wrap(err, "failed to compile"), 1) + } + } + v := vm.New() + v.LoadScript(b) + v.PrintOps() + return nil } diff --git a/cli/vm/vm.go b/cli/vm/vm.go index 6e0876723..3dab28638 100644 --- a/cli/vm/vm.go +++ b/cli/vm/vm.go @@ -1,10 +1,6 @@ package vm import ( - "errors" - "io/ioutil" - - "github.com/CityOfZion/neo-go/pkg/vm" vmcli "github.com/CityOfZion/neo-go/pkg/vm/cli" "github.com/urfave/cli" ) @@ -18,19 +14,6 @@ func NewCommands() []cli.Command { Flags: []cli.Flag{ cli.BoolFlag{Name: "debug, d"}, }, - Subcommands: []cli.Command{ - { - Name: "inspect", - Usage: "dump instructions of the avm file given", - Action: inspect, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "in, i", - Usage: "input file of the program (AVM)", - }, - }, - }, - }, }} } @@ -38,18 +21,3 @@ func startVMPrompt(ctx *cli.Context) error { p := vmcli.New() return p.Run() } - -func inspect(ctx *cli.Context) error { - avm := ctx.String("in") - if len(avm) == 0 { - return cli.NewExitError(errors.New("no input file given"), 1) - } - b, err := ioutil.ReadFile(avm) - if err != nil { - return cli.NewExitError(err, 1) - } - v := vm.New() - v.LoadScript(b) - v.PrintOps() - return nil -} diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 4427d2f08..59b82cca2 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -432,19 +432,9 @@ func (bc *Blockchain) storeBlock(block *Block) error { contracts[contract.ScriptHash()] = contract case *transaction.InvocationTX: - vm := vm.New() - vm.SetCheckedHash(tx.VerificationHash().Bytes()) - vm.SetScriptGetter(func(hash util.Uint160) []byte { - cs := bc.GetContractState(hash) - if cs == nil { - return nil - } - - return cs.Script - }) systemInterop := newInteropContext(0x10, bc, tmpStore, block, tx) - vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) - vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) + vm := bc.spawnVMWithInterops(systemInterop) + vm.SetCheckedHash(tx.VerificationHash().Bytes()) vm.LoadScript(t.Script) err := vm.Run() if !vm.HasFailed() { @@ -1110,6 +1100,30 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([ } +// spawnVMWithInterops returns a VM with script getter and interop functions set +// up for current blockchain. +func (bc *Blockchain) spawnVMWithInterops(interopCtx *interopContext) *vm.VM { + vm := vm.New() + vm.SetScriptGetter(func(hash util.Uint160) []byte { + cs := bc.GetContractState(hash) + if cs == nil { + return nil + } + return cs.Script + }) + vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap()) + vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) + return vm +} + +// GetTestVM returns a VM and a Store setup for a test run of some sort of code. +func (bc *Blockchain) GetTestVM() (*vm.VM, storage.Store) { + tmpStore := storage.NewMemCachedStore(bc.store) + systemInterop := newInteropContext(0x10, bc, tmpStore, nil, nil) + vm := bc.spawnVMWithInterops(systemInterop) + return vm, tmpStore +} + // verifyHashAgainstScript verifies given hash against the given witness. func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error { verification := witness.VerificationScript @@ -1127,17 +1141,8 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa } } - vm := vm.New() + vm := bc.spawnVMWithInterops(interopCtx) vm.SetCheckedHash(checkedHash.Bytes()) - vm.SetScriptGetter(func(hash util.Uint160) []byte { - cs := bc.GetContractState(hash) - if cs == nil { - return nil - } - return cs.Script - }) - vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap()) - vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) vm.LoadScript(verification) vm.LoadScript(witness.InvocationScript) err := vm.Run() diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 1409a2c40..1ce3247e6 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -2,8 +2,10 @@ package core import ( "github.com/CityOfZion/neo-go/config" + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" ) // Blockchainer is an interface that abstract the implementation @@ -27,6 +29,7 @@ type Blockchainer interface { GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) + GetTestVM() (*vm.VM, storage.Store) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetUnspentCoinState(util.Uint256) *UnspentCoinState References(t *transaction.Transaction) map[transaction.Input]*transaction.Output diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index dbb55f607..2ad71205d 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -9,9 +9,11 @@ import ( "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/network/payload" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" ) type testChain struct { @@ -78,6 +80,9 @@ func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([] func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem { panic("TODO") } +func (chain testChain) GetTestVM() (*vm.VM, storage.Store) { + panic("TODO") +} func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*core.StorageItem, error) { panic("TODO") } diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 24c203d30..1ea607d6e 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -192,10 +192,6 @@ Methods: results = peers - case "getblocksysfee", "getcontractstate", "getrawmempool", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript": - - results = "TODO" - case "validateaddress": validateaddressCalled.Inc() param, err := reqParams.Value(0) @@ -242,6 +238,9 @@ Methods: getrawtransactionCalled.Inc() results, resultsErr = s.getrawtransaction(reqParams) + case "invokescript": + results, resultsErr = s.invokescript(reqParams) + case "sendrawtransaction": sendrawtransactionCalled.Inc() results, resultsErr = s.sendrawtransaction(reqParams) @@ -296,6 +295,28 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { return results, resultsErr } +// invokescript implements the `invokescript` RPC call. +func (s *Server) invokescript(reqParams Params) (interface{}, error) { + hexScript, err := reqParams.ValueWithType(0, "string") + if err != nil { + return nil, err + } + script, err := hex.DecodeString(hexScript.StringVal) + if err != nil { + return nil, err + } + vm, _ := s.chain.GetTestVM() + vm.LoadScript(script) + _ = vm.Run() + result := &wrappers.InvokeResult{ + State: vm.State(), + GasConsumed: "0.1", + Script: hexScript.StringVal, + Stack: vm.Estack(), + } + return result, nil +} + func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) { var resultsErr error var results interface{} diff --git a/pkg/rpc/wrappers/invoke_result.go b/pkg/rpc/wrappers/invoke_result.go new file mode 100644 index 000000000..4cc790bf5 --- /dev/null +++ b/pkg/rpc/wrappers/invoke_result.go @@ -0,0 +1,13 @@ +package wrappers + +import ( + "github.com/CityOfZion/neo-go/pkg/vm" +) + +// InvokeResult is used as a wrapper to represent an invokation result. +type InvokeResult struct { + State string `json:"state"` + GasConsumed string `json:"gas_consumed"` + Script string `json:"script"` + Stack *vm.Stack +} diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 3083e0b6e..e4e2115ba 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -259,7 +259,7 @@ func handleLoadGo(c *ishell.Context) { c.Err(err) return } - b, err := compiler.Compile(bytes.NewReader(fb), &compiler.Options{}) + b, err := compiler.Compile(bytes.NewReader(fb)) if err != nil { c.Err(err) return diff --git a/pkg/vm/compiler/compiler.go b/pkg/vm/compiler/compiler.go index 7a264650f..5ead0d34a 100644 --- a/pkg/vm/compiler/compiler.go +++ b/pkg/vm/compiler/compiler.go @@ -14,7 +14,6 @@ import ( "os" "strings" - "github.com/CityOfZion/neo-go/pkg/vm" "golang.org/x/tools/go/loader" ) @@ -38,7 +37,7 @@ type buildInfo struct { } // Compile compiles a Go program into bytecode that can run on the NEO virtual machine. -func Compile(r io.Reader, o *Options) ([]byte, error) { +func Compile(r io.Reader) ([]byte, error) { conf := loader.Config{ParserMode: parser.ParseComments} f, err := conf.ParseFile("", r) if err != nil { @@ -85,7 +84,7 @@ func CompileAndSave(src string, o *Options) error { if err != nil { return err } - b, err = Compile(bytes.NewReader(b), o) + b, err = Compile(bytes.NewReader(b)) if err != nil { return fmt.Errorf("error while trying to compile smart contract file: %v", err) } @@ -96,23 +95,6 @@ func CompileAndSave(src string, o *Options) error { return ioutil.WriteFile(out, b, os.ModePerm) } -// CompileAndInspect compiles the program and dumps the opcode in a user friendly format. -func CompileAndInspect(src string) error { - b, err := ioutil.ReadFile(src) - if err != nil { - return err - } - b, err = Compile(bytes.NewReader(b), &Options{}) - if err != nil { - return err - } - - v := vm.New() - v.LoadScript(b) - v.PrintOps() - return nil -} - func gopath() string { gopath := os.Getenv("GOPATH") if len(gopath) == 0 { diff --git a/pkg/vm/compiler/compiler_test.go b/pkg/vm/compiler/compiler_test.go index 1ab46d1ae..5ddd11824 100644 --- a/pkg/vm/compiler/compiler_test.go +++ b/pkg/vm/compiler/compiler_test.go @@ -44,14 +44,10 @@ func filterFilename(infos []os.FileInfo) string { } func compileFile(src string) error { - o := compiler.Options{ - Outfile: "tmp/contract.avm", - } - file, err := os.Open(src) if err != nil { return err } - _, err = compiler.Compile(file, &o) + _, err = compiler.Compile(file) return err } diff --git a/pkg/vm/output.go b/pkg/vm/output.go index 4cad005d8..14e83103b 100644 --- a/pkg/vm/output.go +++ b/pkg/vm/output.go @@ -9,7 +9,7 @@ type stackItem struct { Type string `json:"type"` } -func buildStackOutput(s *Stack) string { +func stackToArray(s *Stack) []stackItem { items := make([]stackItem, 0, s.Len()) s.Iter(func(e *Element) { items = append(items, stackItem{ @@ -17,7 +17,10 @@ func buildStackOutput(s *Stack) string { Type: e.value.String(), }) }) + return items +} - b, _ := json.MarshalIndent(items, "", " ") +func buildStackOutput(s *Stack) string { + b, _ := json.MarshalIndent(stackToArray(s), "", " ") return string(b) } diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index f295d1e59..13a6c0f75 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -1,6 +1,7 @@ package vm import ( + "encoding/json" "fmt" "math/big" @@ -334,3 +335,8 @@ func (s *Stack) popSigElements() ([][]byte, error) { } return elems, nil } + +// MarshalJSON implements JSON marshalling interface. +func (s *Stack) MarshalJSON() ([]byte, error) { + return json.Marshal(stackToArray(s)) +} diff --git a/pkg/vm/tests/vm_test.go b/pkg/vm/tests/vm_test.go index a65357e67..deb18eaa7 100644 --- a/pkg/vm/tests/vm_test.go +++ b/pkg/vm/tests/vm_test.go @@ -52,7 +52,7 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put, 1) vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext, 1) - b, err := compiler.Compile(strings.NewReader(src), &compiler.Options{}) + b, err := compiler.Compile(strings.NewReader(src)) if err != nil { t.Fatal(err) } diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index f425a5a6f..568e47110 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -199,10 +199,11 @@ func (v *VM) LoadFile(path string) error { // Load initializes the VM with the program given. func (v *VM) Load(prog []byte) { - // clear all stacks, it could be a reload. + // Clear all stacks and state, it could be a reload. v.istack.Clear() v.estack.Clear() v.astack.Clear() + v.state = noneState v.LoadScript(prog) } @@ -250,6 +251,11 @@ func (v *VM) Stack(n string) string { return buildStackOutput(s) } +// State returns string representation of the state for the VM. +func (v *VM) State() string { + return v.state.String() +} + // Ready returns true if the VM ready to execute the loaded program. // Will return false if no program is loaded. func (v *VM) Ready() bool {