vm: removed mute mode and pushed logging to upper lvl

VM should be responsible for code execution and in case anyone interested in additional logging or errors they could handle them like we do it iin cli.
This commit is contained in:
Vsevolod Brekelov 2019-10-22 13:44:14 +03:00
parent f2805541cb
commit e2bfff8666
7 changed files with 326 additions and 456 deletions

View file

@ -48,7 +48,7 @@ func inspect(ctx *cli.Context) error {
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
v := vm.New(0) v := vm.New()
v.LoadScript(b) v.LoadScript(b)
v.PrintOps() v.PrintOps()
return nil return nil

View file

@ -407,7 +407,7 @@ 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.ModeMute) vm := vm.New()
vm.SetCheckedHash(tx.VerificationHash().Bytes()) vm.SetCheckedHash(tx.VerificationHash().Bytes())
vm.SetScriptGetter(func(hash util.Uint160) []byte { vm.SetScriptGetter(func(hash util.Uint160) []byte {
cs := bc.GetContractState(hash) cs := bc.GetContractState(hash)
@ -421,7 +421,7 @@ func (bc *Blockchain) storeBlock(block *Block) error {
vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap())
vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap())
vm.LoadScript(t.Script) vm.LoadScript(t.Script)
vm.Run() err := vm.Run()
if !vm.HasFailed() { if !vm.HasFailed() {
_, err := systemInterop.mem.Persist() _, err := systemInterop.mem.Persist()
if err != nil { if err != nil {
@ -431,6 +431,7 @@ func (bc *Blockchain) storeBlock(block *Block) error {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"tx": tx.Hash().ReverseString(), "tx": tx.Hash().ReverseString(),
"block": block.Index, "block": block.Index,
"err": err,
}).Warn("contract invocation failed") }).Warn("contract invocation failed")
} }
} }
@ -1091,7 +1092,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
} }
} }
vm := vm.New(vm.ModeMute) vm := vm.New()
vm.SetCheckedHash(checkedHash.Bytes()) vm.SetCheckedHash(checkedHash.Bytes())
vm.SetScriptGetter(func(hash util.Uint160) []byte { vm.SetScriptGetter(func(hash util.Uint160) []byte {
cs := bc.GetContractState(hash) cs := bc.GetContractState(hash)
@ -1104,9 +1105,9 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap())
vm.LoadScript(verification) vm.LoadScript(verification)
vm.LoadScript(witness.InvocationScript) vm.LoadScript(witness.InvocationScript)
vm.Run() err := vm.Run()
if vm.HasFailed() { if vm.HasFailed() {
return errors.Errorf("vm failed to execute the script") return errors.Errorf("vm failed to execute the script with error: %s", err)
} }
resEl := vm.Estack().Pop() resEl := vm.Estack().Pop()
if resEl != nil { if resEl != nil {

View file

@ -169,7 +169,7 @@ type VMCLI struct {
// New returns a new VMCLI object. // New returns a new VMCLI object.
func New() *VMCLI { func New() *VMCLI {
vmcli := VMCLI{ vmcli := VMCLI{
vm: vm.New(0), vm: vm.New(),
shell: ishell.New(), shell: ishell.New(),
} }
vmcli.shell.Set(vmKey, vmcli.vm) vmcli.shell.Set(vmKey, vmcli.vm)
@ -286,16 +286,40 @@ func handleRun(c *ishell.Context) {
} }
v.LoadArgs(method, params) v.LoadArgs(method, params)
} }
v.Run() runVMWithHandling(c, v)
changePrompt(c, v) changePrompt(c, v)
} }
// runVMWithHandling runs VM with handling errors and additional state messages.
func runVMWithHandling(c *ishell.Context, v *vm.VM) {
err := v.Run()
if err != nil {
c.Err(err)
return
}
var message string
switch {
case v.HasFailed():
message = "FAILED"
case v.HasHalted():
message = v.Stack("estack")
case v.AtBreakpoint():
ctx := v.Context()
i, op := ctx.CurrInstr()
message = fmt.Sprintf("at breakpoint %d (%s)\n", i, op.String())
}
if message != "" {
c.Printf(message)
}
}
func handleCont(c *ishell.Context) { func handleCont(c *ishell.Context) {
if !checkVMIsReady(c) { if !checkVMIsReady(c) {
return return
} }
v := getVMFromContext(c) v := getVMFromContext(c)
v.Run() runVMWithHandling(c, v)
changePrompt(c, v) changePrompt(c, v)
} }
@ -317,7 +341,7 @@ func handleStep(c *ishell.Context) {
} }
} }
v.AddBreakPointRel(n) v.AddBreakPointRel(n)
v.Run() runVMWithHandling(c, v)
changePrompt(c, v) changePrompt(c, v)
} }
@ -338,14 +362,19 @@ func handleStepType(c *ishell.Context, stepType string) {
return return
} }
v := getVMFromContext(c) v := getVMFromContext(c)
var err error
switch stepType { switch stepType {
case "into": case "into":
v.StepInto() err = v.StepInto()
case "out": case "out":
v.StepOut() err = v.StepOut()
case "over": case "over":
v.StepOver() err = v.StepOver()
} }
if err != nil {
c.Err(err)
}
handleIP(c)
changePrompt(c, v) changePrompt(c, v)
} }

View file

@ -107,7 +107,7 @@ func CompileAndInspect(src string) error {
return err return err
} }
v := vm.New(0) v := vm.New()
v.LoadScript(b) v.LoadScript(b)
v.PrintOps() v.PrintOps()
return nil return nil

View file

@ -8,6 +8,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/vm" "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/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type testCase struct { type testCase struct {
@ -24,14 +25,16 @@ func run_testcases(t *testing.T, tcases []testCase) {
func eval(t *testing.T, src string, result interface{}) { func eval(t *testing.T, src string, result interface{}) {
vm := vmAndCompile(t, src) vm := vmAndCompile(t, src)
vm.Run() err := vm.Run()
require.NoError(t, err)
assertResult(t, vm, result) assertResult(t, vm, result)
} }
func evalWithArgs(t *testing.T, src string, op []byte, args []vm.StackItem, result interface{}) { func evalWithArgs(t *testing.T, src string, op []byte, args []vm.StackItem, result interface{}) {
vm := vmAndCompile(t, src) vm := vmAndCompile(t, src)
vm.LoadArgs(op, args) vm.LoadArgs(op, args)
vm.Run() err := vm.Run()
require.NoError(t, err)
assertResult(t, vm, result) assertResult(t, vm, result)
} }
@ -42,7 +45,7 @@ func assertResult(t *testing.T, vm *vm.VM, result interface{}) {
} }
func vmAndCompile(t *testing.T, src string) *vm.VM { func vmAndCompile(t *testing.T, src string) *vm.VM {
vm := vm.New(vm.ModeMute) vm := vm.New()
storePlugin := newStoragePlugin() storePlugin := newStoragePlugin()
vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get, 1) vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get, 1)

View file

@ -5,7 +5,6 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"math/big" "math/big"
"os" "os"
"reflect" "reflect"
@ -15,15 +14,25 @@ import (
"github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/pkg/errors"
) )
// Mode configures behaviour of the VM. type errorAtInstruct struct {
type Mode uint ip int
op Instruction
err interface{}
}
// Available VM Modes. func (e *errorAtInstruct) Error() string {
var ( return fmt.Sprintf("error encountered at instruction %d (%s): %s", e.ip, e.op, e.err)
ModeMute Mode = 1 << 0 }
)
func newError(ip int, op Instruction, err interface{}) *errorAtInstruct {
return &errorAtInstruct{ip: ip, op: op, err: err}
}
// StateMessage is a vm state message which could be used as additional info for example by cli.
type StateMessage string
const ( const (
// MaxArraySize is the maximum array size allowed in the VM. // MaxArraySize is the maximum array size allowed in the VM.
@ -46,8 +55,6 @@ type VM struct {
estack *Stack // execution stack. estack *Stack // execution stack.
astack *Stack // alt stack. astack *Stack // alt stack.
// Mute all output after execution.
mute bool
// Hash to verify in CHECKSIG/CHECKMULTISIG. // Hash to verify in CHECKSIG/CHECKMULTISIG.
checkhash []byte checkhash []byte
} }
@ -59,7 +66,7 @@ type InteropFuncPrice struct {
} }
// New returns a new VM object ready to load .avm bytecode scripts. // New returns a new VM object ready to load .avm bytecode scripts.
func New(mode Mode) *VM { func New() *VM {
vm := &VM{ vm := &VM{
interop: make(map[string]InteropFuncPrice), interop: make(map[string]InteropFuncPrice),
getScript: nil, getScript: nil,
@ -68,9 +75,6 @@ func New(mode Mode) *VM {
estack: NewStack("evaluation"), estack: NewStack("evaluation"),
astack: NewStack("alt"), astack: NewStack("alt"),
} }
if mode == ModeMute {
vm.mute = true
}
// Register native interop hooks. // Register native interop hooks.
vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog, 1) vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog, 1)
@ -244,10 +248,10 @@ func (v *VM) Ready() bool {
} }
// Run starts the execution of the loaded program. // Run starts the execution of the loaded program.
func (v *VM) Run() { func (v *VM) Run() error {
if !v.Ready() { if !v.Ready() {
fmt.Println("no program loaded") v.state = faultState
return return errors.New("no program loaded")
} }
v.state = noneState v.state = noneState
@ -258,40 +262,33 @@ func (v *VM) Run() {
v.state |= breakState v.state |= breakState
} }
switch { switch {
case v.state.HasFlag(faultState): case v.state.HasFlag(faultState), v.state.HasFlag(haltState), v.state.HasFlag(breakState):
fmt.Println("FAULT") return errors.New("VM stopped")
return
case v.state.HasFlag(haltState):
if !v.mute {
fmt.Println(v.Stack("estack"))
}
return
case v.state.HasFlag(breakState):
ctx := v.Context()
i, op := ctx.CurrInstr()
fmt.Printf("at breakpoint %d (%s)\n", i, op.String())
return
case v.state == noneState: case v.state == noneState:
v.Step() if err := v.Step(); err != nil {
return err
}
default:
v.state = faultState
return errors.New("unknown state")
} }
} }
} }
// Step 1 instruction in the program. // Step 1 instruction in the program.
func (v *VM) Step() { func (v *VM) Step() error {
ctx := v.Context() ctx := v.Context()
op, param, err := ctx.Next() op, param, err := ctx.Next()
if err != nil { if err != nil {
log.Printf("error encountered at instruction %d (%s)", ctx.ip, op)
log.Println(err)
v.state = faultState v.state = faultState
return newError(ctx.ip, op, err)
} }
v.execute(ctx, op, param) return v.execute(ctx, op, param)
} }
// StepInto behaves the same as “step over” in case if the line does not contain a function. Otherwise // StepInto behaves the same as “step over” in case if the line does not contain a function. Otherwise
// the debugger will enter the called function and continue line-by-line debugging there. // the debugger will enter the called function and continue line-by-line debugging there.
func (v *VM) StepInto() { func (v *VM) StepInto() error {
ctx := v.Context() ctx := v.Context()
if ctx == nil { if ctx == nil {
@ -299,29 +296,31 @@ func (v *VM) StepInto() {
} }
if v.HasStopped() { if v.HasStopped() {
return return nil
} }
if ctx != nil && ctx.prog != nil { if ctx != nil && ctx.prog != nil {
op, param, err := ctx.Next() op, param, err := ctx.Next()
if err != nil { if err != nil {
log.Printf("error encountered at instruction %d (%s)", ctx.ip, op)
log.Println(err)
v.state = faultState v.state = faultState
return newError(ctx.ip, op, err)
}
vErr := v.execute(ctx, op, param)
if vErr != nil {
return vErr
} }
v.execute(ctx, op, param)
i, op := ctx.CurrInstr()
fmt.Printf("at breakpoint %d (%s)\n", i, op.String())
} }
cctx := v.Context() cctx := v.Context()
if cctx != nil && cctx.atBreakPoint() { if cctx != nil && cctx.atBreakPoint() {
v.state = breakState v.state = breakState
} }
return nil
} }
// StepOut takes the debugger to the line where the current function was called. // StepOut takes the debugger to the line where the current function was called.
func (v *VM) StepOut() { func (v *VM) StepOut() error {
var err error
if v.state == breakState { if v.state == breakState {
v.state = noneState v.state = noneState
} else { } else {
@ -330,15 +329,17 @@ func (v *VM) StepOut() {
expSize := v.istack.len expSize := v.istack.len
for v.state.HasFlag(noneState) && v.istack.len >= expSize { for v.state.HasFlag(noneState) && v.istack.len >= expSize {
v.StepInto() err = v.StepInto()
} }
return err
} }
// StepOver takes the debugger to the line that will step over a given line. // StepOver takes the debugger to the line that will step over a given line.
// If the line contains a function the function will be executed and the result returned without debugging each line. // If the line contains a function the function will be executed and the result returned without debugging each line.
func (v *VM) StepOver() { func (v *VM) StepOver() error {
var err error
if v.HasStopped() { if v.HasStopped() {
return return err
} }
if v.state == breakState { if v.state == breakState {
@ -349,11 +350,12 @@ func (v *VM) StepOver() {
expSize := v.istack.len expSize := v.istack.len
for { for {
v.StepInto() err = v.StepInto()
if !(v.state.HasFlag(noneState) && v.istack.len > expSize) { if !(v.state.HasFlag(noneState) && v.istack.len > expSize) {
break break
} }
} }
return err
} }
// HasFailed returns whether VM is in the failed state now. Usually used to // HasFailed returns whether VM is in the failed state now. Usually used to
@ -367,6 +369,16 @@ func (v *VM) HasStopped() bool {
return v.state.HasFlag(haltState) || v.state.HasFlag(faultState) return v.state.HasFlag(haltState) || v.state.HasFlag(faultState)
} }
// HasHalted returns whether VM is in Halt state.
func (v *VM) HasHalted() bool {
return v.state.HasFlag(haltState)
}
// AtBreakpoint returns whether VM is at breakpoint.
func (v *VM) AtBreakpoint() bool {
return v.state.HasFlag(breakState)
}
// SetCheckedHash sets checked hash for CHECKSIG and CHECKMULTISIG instructions. // SetCheckedHash sets checked hash for CHECKSIG and CHECKMULTISIG instructions.
func (v *VM) SetCheckedHash(h []byte) { func (v *VM) SetCheckedHash(h []byte) {
v.checkhash = make([]byte, len(h)) v.checkhash = make([]byte, len(h))
@ -379,14 +391,13 @@ func (v *VM) SetScriptGetter(gs func(util.Uint160) []byte) {
} }
// execute performs an instruction cycle in the VM. Acting on the instruction (opcode). // execute performs an instruction cycle in the VM. Acting on the instruction (opcode).
func (v *VM) execute(ctx *Context, op Instruction, parameter []byte) { func (v *VM) execute(ctx *Context, op Instruction, parameter []byte) (err error) {
// Instead of polluting the whole VM logic with error handling, we will recover // Instead of polluting the whole VM logic with error handling, we will recover
// each panic at a central point, putting the VM in a fault state. // each panic at a central point, putting the VM in a fault state and setting error.
defer func() { defer func() {
if err := recover(); err != nil { if errRecover := recover(); errRecover != nil {
log.Printf("error encountered at instruction %d (%s)", ctx.ip, op)
log.Println(err)
v.state = faultState v.state = faultState
err = newError(ctx.ip, op, errRecover)
} }
}() }()
@ -961,7 +972,10 @@ func (v *VM) execute(ctx *Context, op Instruction, parameter []byte) {
case CALL: case CALL:
v.istack.PushVal(ctx.Copy()) v.istack.PushVal(ctx.Copy())
v.execute(v.Context(), JMP, parameter) err = v.execute(v.Context(), JMP, parameter)
if err != nil {
return
}
case SYSCALL: case SYSCALL:
ifunc, ok := v.interop[string(parameter)] ifunc, ok := v.interop[string(parameter)]
@ -1161,6 +1175,7 @@ func (v *VM) execute(ctx *Context, op Instruction, parameter []byte) {
default: default:
panic(fmt.Sprintf("unknown opcode %s", op.String())) panic(fmt.Sprintf("unknown opcode %s", op.String()))
} }
return
} }
func cloneIfStruct(item StackItem) StackItem { func cloneIfStruct(item StackItem) StackItem {
@ -1189,8 +1204,3 @@ func validateMapKey(key *Element) {
panic("key can't be a collection") panic("key can't be a collection")
} }
} }
func init() {
log.SetPrefix("NEO-GO-VM > ")
log.SetFlags(0)
}

File diff suppressed because it is too large Load diff