Merge pull request #463 from nspcc-dev/smartcontract-fixes

Smartcontract RPC fixes
This commit is contained in:
Roman Khimov 2019-10-29 20:54:46 +03:00 committed by GitHub
commit 2f6e678a19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 126 additions and 96 deletions

View file

@ -6,18 +6,20 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/CityOfZion/neo-go/pkg/rpc" "github.com/CityOfZion/neo-go/pkg/rpc"
"github.com/CityOfZion/neo-go/pkg/vm"
"github.com/CityOfZion/neo-go/pkg/vm/compiler" "github.com/CityOfZion/neo-go/pkg/vm/compiler"
"github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
var ( 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") 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") 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") 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", Usage: "Test an invocation of a smart contract on the blockchain",
Action: testInvoke, Action: testInvoke,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{
Name: "endpoint, e",
Usage: "RPC endpoint address (like 'http://seed4.ngd.network:20332')",
},
cli.StringFlag{ cli.StringFlag{
Name: "in, i", Name: "in, i",
Usage: "Input location of the avm file that needs to be invoked", 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", Usage: "creates a user readable dump of the program instructions",
Action: inspect, Action: inspect,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{
Name: "compile, c",
Usage: "compile input file (it should be go code then)",
},
cli.StringFlag{ cli.StringFlag{
Name: "in, i", Name: "in, i",
Usage: "input file of the program", Usage: "input file of the program",
@ -163,16 +173,16 @@ func testInvoke(ctx *cli.Context) error {
if len(src) == 0 { if len(src) == 0 {
return cli.NewExitError(errNoInput, 1) return cli.NewExitError(errNoInput, 1)
} }
endpoint := ctx.String("endpoint")
if len(endpoint) == 0 {
return cli.NewExitError(errNoEndpoint, 1)
}
b, err := ioutil.ReadFile(src) b, err := ioutil.ReadFile(src)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) 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{}) client, err := rpc.NewClient(context.TODO(), endpoint, rpc.ClientOptions{})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
@ -251,12 +261,24 @@ func parseContractDetails() ContractDetails {
} }
func inspect(ctx *cli.Context) error { func inspect(ctx *cli.Context) error {
src := ctx.String("in") in := ctx.String("in")
if len(src) == 0 { compile := ctx.Bool("compile")
if len(in) == 0 {
return cli.NewExitError(errNoInput, 1) 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) 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 return nil
} }

View file

@ -1,10 +1,6 @@
package vm package vm
import ( import (
"errors"
"io/ioutil"
"github.com/CityOfZion/neo-go/pkg/vm"
vmcli "github.com/CityOfZion/neo-go/pkg/vm/cli" vmcli "github.com/CityOfZion/neo-go/pkg/vm/cli"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -18,19 +14,6 @@ func NewCommands() []cli.Command {
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{Name: "debug, d"}, 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() p := vmcli.New()
return p.Run() 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
}

View file

@ -432,19 +432,9 @@ func (bc *Blockchain) storeBlock(block *Block) error {
contracts[contract.ScriptHash()] = contract contracts[contract.ScriptHash()] = contract
case *transaction.InvocationTX: 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) systemInterop := newInteropContext(0x10, bc, tmpStore, block, tx)
vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) vm := bc.spawnVMWithInterops(systemInterop)
vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) vm.SetCheckedHash(tx.VerificationHash().Bytes())
vm.LoadScript(t.Script) vm.LoadScript(t.Script)
err := vm.Run() err := vm.Run()
if !vm.HasFailed() { 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. // verifyHashAgainstScript verifies given hash against the given witness.
func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error { func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error {
verification := witness.VerificationScript 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.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(verification)
vm.LoadScript(witness.InvocationScript) vm.LoadScript(witness.InvocationScript)
err := vm.Run() err := vm.Run()

View file

@ -2,8 +2,10 @@ package core
import ( import (
"github.com/CityOfZion/neo-go/config" "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/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
) )
// Blockchainer is an interface that abstract the implementation // Blockchainer is an interface that abstract the implementation
@ -27,6 +29,7 @@ type Blockchainer interface {
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem
GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error)
GetTestVM() (*vm.VM, storage.Store)
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
GetUnspentCoinState(util.Uint256) *UnspentCoinState GetUnspentCoinState(util.Uint256) *UnspentCoinState
References(t *transaction.Transaction) map[transaction.Input]*transaction.Output References(t *transaction.Transaction) map[transaction.Input]*transaction.Output

View file

@ -9,9 +9,11 @@ import (
"github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/config"
"github.com/CityOfZion/neo-go/pkg/core" "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/core/transaction"
"github.com/CityOfZion/neo-go/pkg/network/payload" "github.com/CityOfZion/neo-go/pkg/network/payload"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
) )
type testChain struct { type testChain struct {
@ -78,6 +80,9 @@ func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]
func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem { func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem {
panic("TODO") panic("TODO")
} }
func (chain testChain) GetTestVM() (*vm.VM, storage.Store) {
panic("TODO")
}
func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*core.StorageItem, error) { func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*core.StorageItem, error) {
panic("TODO") panic("TODO")
} }

View file

@ -192,10 +192,6 @@ Methods:
results = peers results = peers
case "getblocksysfee", "getcontractstate", "getrawmempool", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript":
results = "TODO"
case "validateaddress": case "validateaddress":
validateaddressCalled.Inc() validateaddressCalled.Inc()
param, err := reqParams.Value(0) param, err := reqParams.Value(0)
@ -242,6 +238,9 @@ Methods:
getrawtransactionCalled.Inc() getrawtransactionCalled.Inc()
results, resultsErr = s.getrawtransaction(reqParams) results, resultsErr = s.getrawtransaction(reqParams)
case "invokescript":
results, resultsErr = s.invokescript(reqParams)
case "sendrawtransaction": case "sendrawtransaction":
sendrawtransactionCalled.Inc() sendrawtransactionCalled.Inc()
results, resultsErr = s.sendrawtransaction(reqParams) results, resultsErr = s.sendrawtransaction(reqParams)
@ -296,6 +295,28 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) {
return results, resultsErr 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) { func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) {
var resultsErr error var resultsErr error
var results interface{} var results interface{}

View file

@ -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
}

View file

@ -259,7 +259,7 @@ func handleLoadGo(c *ishell.Context) {
c.Err(err) c.Err(err)
return return
} }
b, err := compiler.Compile(bytes.NewReader(fb), &compiler.Options{}) b, err := compiler.Compile(bytes.NewReader(fb))
if err != nil { if err != nil {
c.Err(err) c.Err(err)
return return

View file

@ -14,7 +14,6 @@ import (
"os" "os"
"strings" "strings"
"github.com/CityOfZion/neo-go/pkg/vm"
"golang.org/x/tools/go/loader" "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. // 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} conf := loader.Config{ParserMode: parser.ParseComments}
f, err := conf.ParseFile("", r) f, err := conf.ParseFile("", r)
if err != nil { if err != nil {
@ -85,7 +84,7 @@ func CompileAndSave(src string, o *Options) error {
if err != nil { if err != nil {
return err return err
} }
b, err = Compile(bytes.NewReader(b), o) b, err = Compile(bytes.NewReader(b))
if err != nil { if err != nil {
return fmt.Errorf("error while trying to compile smart contract file: %v", err) 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) 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 { func gopath() string {
gopath := os.Getenv("GOPATH") gopath := os.Getenv("GOPATH")
if len(gopath) == 0 { if len(gopath) == 0 {

View file

@ -44,14 +44,10 @@ func filterFilename(infos []os.FileInfo) string {
} }
func compileFile(src string) error { func compileFile(src string) error {
o := compiler.Options{
Outfile: "tmp/contract.avm",
}
file, err := os.Open(src) file, err := os.Open(src)
if err != nil { if err != nil {
return err return err
} }
_, err = compiler.Compile(file, &o) _, err = compiler.Compile(file)
return err return err
} }

View file

@ -9,7 +9,7 @@ type stackItem struct {
Type string `json:"type"` Type string `json:"type"`
} }
func buildStackOutput(s *Stack) string { func stackToArray(s *Stack) []stackItem {
items := make([]stackItem, 0, s.Len()) items := make([]stackItem, 0, s.Len())
s.Iter(func(e *Element) { s.Iter(func(e *Element) {
items = append(items, stackItem{ items = append(items, stackItem{
@ -17,7 +17,10 @@ func buildStackOutput(s *Stack) string {
Type: e.value.String(), Type: e.value.String(),
}) })
}) })
return items
}
b, _ := json.MarshalIndent(items, "", " ") func buildStackOutput(s *Stack) string {
b, _ := json.MarshalIndent(stackToArray(s), "", " ")
return string(b) return string(b)
} }

View file

@ -1,6 +1,7 @@
package vm package vm
import ( import (
"encoding/json"
"fmt" "fmt"
"math/big" "math/big"
@ -334,3 +335,8 @@ func (s *Stack) popSigElements() ([][]byte, error) {
} }
return elems, nil return elems, nil
} }
// MarshalJSON implements JSON marshalling interface.
func (s *Stack) MarshalJSON() ([]byte, error) {
return json.Marshal(stackToArray(s))
}

View file

@ -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.Put", storePlugin.Put, 1)
vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext, 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -199,10 +199,11 @@ func (v *VM) LoadFile(path string) error {
// Load initializes the VM with the program given. // Load initializes the VM with the program given.
func (v *VM) Load(prog []byte) { 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.istack.Clear()
v.estack.Clear() v.estack.Clear()
v.astack.Clear() v.astack.Clear()
v.state = noneState
v.LoadScript(prog) v.LoadScript(prog)
} }
@ -250,6 +251,11 @@ func (v *VM) Stack(n string) string {
return buildStackOutput(s) 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. // Ready returns true if the VM ready to execute the loaded program.
// Will return false if no program is loaded. // Will return false if no program is loaded.
func (v *VM) Ready() bool { func (v *VM) Ready() bool {