Merge pull request #2270 from nspcc-dev/vm-invoked-contracts

Add invoked contract tracing
This commit is contained in:
Roman Khimov 2021-12-01 11:27:27 +03:00 committed by GitHub
commit 01d15ff473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 362 additions and 169 deletions

View file

@ -42,7 +42,12 @@ func eval(t *testing.T, src string, result interface{}) {
func evalWithArgs(t *testing.T, src string, op []byte, args []stackitem.Item, result interface{}) { func evalWithArgs(t *testing.T, src string, op []byte, args []stackitem.Item, result interface{}) {
vm := vmAndCompile(t, src) vm := vmAndCompile(t, src)
vm.LoadArgs(op, args) if len(args) > 0 {
vm.Estack().PushVal(args)
}
if op != nil {
vm.Estack().PushVal(op)
}
err := vm.Run() err := vm.Run()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, vm.Estack().Len(), "stack contains unexpected items") assert.Equal(t, 1, vm.Estack().Len(), "stack contains unexpected items")
@ -87,9 +92,9 @@ func invokeMethod(t *testing.T, method string, script []byte, v *vm.VM, di *comp
} }
require.True(t, mainOffset >= 0) require.True(t, mainOffset >= 0)
v.LoadScriptWithFlags(script, callflag.All) v.LoadScriptWithFlags(script, callflag.All)
v.Jump(v.Context(), mainOffset) v.Context().Jump(mainOffset)
if initOffset >= 0 { if initOffset >= 0 {
v.Call(v.Context(), initOffset) v.Call(initOffset)
} }
} }

View file

@ -2213,14 +2213,14 @@ func (bc *Blockchain) InitVerificationVM(v *vm.VM, getContract func(util.Uint160
if md == nil || md.ReturnType != smartcontract.BoolType { if md == nil || md.ReturnType != smartcontract.BoolType {
return ErrInvalidVerificationContract return ErrInvalidVerificationContract
} }
initMD := cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0) verifyOffset := md.Offset
v.LoadScriptWithHash(cs.NEF.Script, hash, callflag.ReadOnly) initOffset := -1
v.Context().NEF = &cs.NEF md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0)
v.Jump(v.Context(), md.Offset) if md != nil {
initOffset = md.Offset
if initMD != nil {
v.Call(v.Context(), initMD.Offset)
} }
v.LoadNEFMethod(&cs.NEF, util.Uint160{}, hash, callflag.ReadOnly,
true, verifyOffset, initOffset)
} }
if len(witness.InvocationScript) != 0 { if len(witness.InvocationScript) != 0 {
err := vm.IsScriptCorrect(witness.InvocationScript, nil) err := vm.IsScriptCorrect(witness.InvocationScript, nil)

View file

@ -48,6 +48,7 @@ type Context struct {
Log *zap.Logger Log *zap.Logger
VM *vm.VM VM *vm.VM
Functions []Function Functions []Function
Invocations map[util.Uint160]int
cancelFuncs []context.CancelFunc cancelFuncs []context.CancelFunc
getContract func(dao.DAO, util.Uint160) (*state.Contract, error) getContract func(dao.DAO, util.Uint160) (*state.Contract, error)
baseExecFee int64 baseExecFee int64
@ -73,6 +74,7 @@ func NewContext(trigger trigger.Type, bc blockchainer.Blockchainer, d dao.DAO,
Tx: tx, Tx: tx,
DAO: dao, DAO: dao,
Log: log, Log: log,
Invocations: make(map[util.Uint160]int),
getContract: getContract, getContract: getContract,
baseExecFee: baseExecFee, baseExecFee: baseExecFee,
} }

View file

@ -112,25 +112,19 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra
return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters)) return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters))
} }
ic.VM.Invocations[cs.Hash]++ methodOff := md.Offset
ic.VM.LoadScriptWithCallingHash(caller, cs.NEF.Script, cs.Hash, ic.VM.Context().GetCallFlags()&f, hasReturn, uint16(len(args))) initOff := -1
ic.VM.Context().NEF = &cs.NEF
for i := len(args) - 1; i >= 0; i-- {
ic.VM.Estack().PushItem(args[i])
}
// use Jump not Call here because context was loaded in LoadScript above.
ic.VM.Jump(ic.VM.Context(), md.Offset)
if hasReturn {
ic.VM.Context().RetCount = 1
} else {
ic.VM.Context().RetCount = 0
}
md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0) md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0)
if md != nil { if md != nil {
ic.VM.Call(ic.VM.Context(), md.Offset) initOff = md.Offset
} }
ic.Invocations[cs.Hash]++
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, ic.VM.Context().GetCallFlags()&f,
hasReturn, methodOff, initOff)
for e, i := ic.VM.Estack(), len(args)-1; i >= 0; i-- {
e.PushItem(args[i])
}
return nil return nil
} }

View file

@ -63,10 +63,10 @@ func GetNotifications(ic *interop.Context) error {
// GetInvocationCounter returns how many times current contract was invoked during current tx execution. // GetInvocationCounter returns how many times current contract was invoked during current tx execution.
func GetInvocationCounter(ic *interop.Context) error { func GetInvocationCounter(ic *interop.Context) error {
currentScriptHash := ic.VM.GetCurrentScriptHash() currentScriptHash := ic.VM.GetCurrentScriptHash()
count, ok := ic.VM.Invocations[currentScriptHash] count, ok := ic.Invocations[currentScriptHash]
if !ok { if !ok {
count = 1 count = 1
ic.VM.Invocations[currentScriptHash] = count ic.Invocations[currentScriptHash] = count
} }
ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(count)))) ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(count))))
return nil return nil

View file

@ -97,9 +97,9 @@ func TestRuntimeGetNotifications(t *testing.T) {
} }
func TestRuntimeGetInvocationCounter(t *testing.T) { func TestRuntimeGetInvocationCounter(t *testing.T) {
ic := &interop.Context{VM: vm.New()} ic := &interop.Context{VM: vm.New(), Invocations: make(map[util.Uint160]int)}
h := random.Uint160() h := random.Uint160()
ic.VM.Invocations[h] = 42 ic.Invocations[h] = 42
t.Run("No invocations", func(t *testing.T) { t.Run("No invocations", func(t *testing.T) {
h1 := h h1 := h

View file

@ -229,21 +229,31 @@ func TestRuntimeGetNotifications(t *testing.T) {
} }
func TestRuntimeGetInvocationCounter(t *testing.T) { func TestRuntimeGetInvocationCounter(t *testing.T) {
v, ic, _ := createVM(t) v, ic, bc := createVM(t)
ic.VM.Invocations[hash.Hash160([]byte{2})] = 42 cs, _ := getTestContractState(bc)
require.NoError(t, bc.contracts.Management.PutContractState(ic.DAO, cs))
ic.Invocations[hash.Hash160([]byte{2})] = 42
t.Run("No invocations", func(t *testing.T) { t.Run("No invocations", func(t *testing.T) {
v.LoadScript([]byte{1}) v.Load([]byte{1})
// do not return an error in this case. // do not return an error in this case.
require.NoError(t, runtime.GetInvocationCounter(ic)) require.NoError(t, runtime.GetInvocationCounter(ic))
require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64())
}) })
t.Run("NonZero", func(t *testing.T) { t.Run("NonZero", func(t *testing.T) {
v.LoadScript([]byte{2}) v.Load([]byte{2})
require.NoError(t, runtime.GetInvocationCounter(ic)) require.NoError(t, runtime.GetInvocationCounter(ic))
require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64())
}) })
t.Run("Contract", func(t *testing.T) {
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, cs.Hash, "invocCounter", callflag.All)
v.LoadWithFlags(w.Bytes(), callflag.All)
require.NoError(t, v.Run())
require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64())
})
} }
func TestStoragePut(t *testing.T) { func TestStoragePut(t *testing.T) {
@ -756,6 +766,9 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) {
burnGasOff := w.Len() burnGasOff := w.Len()
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeBurnGas) emit.Syscall(w.BinWriter, interopnames.SystemRuntimeBurnGas)
emit.Opcodes(w.BinWriter, opcode.RET) emit.Opcodes(w.BinWriter, opcode.RET)
invocCounterOff := w.Len()
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetInvocationCounter)
emit.Opcodes(w.BinWriter, opcode.RET)
script := w.Bytes() script := w.Bytes()
h := hash.Hash160(script) h := hash.Hash160(script)
@ -925,6 +938,11 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) {
}, },
ReturnType: smartcontract.VoidType, ReturnType: smartcontract.VoidType,
}, },
{
Name: "invocCounter",
Offset: invocCounterOff,
ReturnType: smartcontract.IntegerType,
},
} }
m.Permissions = make([]manifest.Permission, 2) m.Permissions = make([]manifest.Permission, 2)
m.Permissions[0].Contract.Type = manifest.PermissionHash m.Permissions[0].Contract.Type = manifest.PermissionHash

View file

@ -243,7 +243,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
v.LoadScriptWithHash(tn.Metadata().NEF.Script, util.Uint160{1, 2, 3}, callflag.All) v.LoadScriptWithHash(tn.Metadata().NEF.Script, util.Uint160{1, 2, 3}, callflag.All)
v.Estack().PushVal(14) v.Estack().PushVal(14)
v.Estack().PushVal(28) v.Estack().PushVal(28)
v.Jump(v.Context(), sumOffset) v.Context().Jump(sumOffset)
// it's prohibited to call natives directly // it's prohibited to call natives directly
require.Error(t, v.Run()) require.Error(t, v.Run())
@ -255,7 +255,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All)
v.Estack().PushVal(14) v.Estack().PushVal(14)
v.Estack().PushVal(28) v.Estack().PushVal(28)
v.Jump(v.Context(), sumOffset) v.Context().Jump(sumOffset)
// it's prohibited to call natives before NativeUpdateHistory[0] height // it's prohibited to call natives before NativeUpdateHistory[0] height
require.Error(t, v.Run()) require.Error(t, v.Run())
@ -269,7 +269,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) {
v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All) v.LoadScriptWithHash(tn.Metadata().NEF.Script, tn.Metadata().Hash, callflag.All)
v.Estack().PushVal(14) v.Estack().PushVal(14)
v.Estack().PushVal(28) v.Estack().PushVal(28)
v.Jump(v.Context(), sumOffset) v.Context().Jump(sumOffset)
require.NoError(t, v.Run()) require.NoError(t, v.Run())
value := v.Estack().Pop().BigInt() value := v.Estack().Pop().BigInt()

View file

@ -3,7 +3,6 @@ package request
import ( import (
"errors" "errors"
"fmt" "fmt"
"strconv"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -103,28 +102,19 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error {
// CreateFunctionInvocationScript creates a script to invoke given contract with // CreateFunctionInvocationScript creates a script to invoke given contract with
// given parameters. // given parameters.
func CreateFunctionInvocationScript(contract util.Uint160, method string, params Params) ([]byte, error) { func CreateFunctionInvocationScript(contract util.Uint160, method string, param *Param) ([]byte, error) {
script := io.NewBufBinWriter() script := io.NewBufBinWriter()
for i := len(params) - 1; i >= 0; i-- { if param == nil {
if slice, err := params[i].GetArray(); err == nil {
err = ExpandArrayIntoScript(script.BinWriter, slice)
if err != nil {
return nil, err
}
emit.Int(script.BinWriter, int64(len(slice)))
emit.Opcodes(script.BinWriter, opcode.PACK)
} else if s, err := params[i].GetStringStrict(); err == nil {
emit.String(script.BinWriter, s)
} else if n, err := params[i].GetIntStrict(); err == nil {
emit.String(script.BinWriter, strconv.Itoa(n))
} else if b, err := params[i].GetBooleanStrict(); err == nil {
emit.Bool(script.BinWriter, b)
} else {
return nil, fmt.Errorf("failed to convert parmeter %s to script parameter", params[i])
}
}
if len(params) == 0 {
emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) emit.Opcodes(script.BinWriter, opcode.NEWARRAY0)
} else if slice, err := param.GetArray(); err == nil {
err = ExpandArrayIntoScript(script.BinWriter, slice)
if err != nil {
return nil, err
}
emit.Int(script.BinWriter, int64(len(slice)))
emit.Opcodes(script.BinWriter, opcode.PACK)
} else {
return nil, fmt.Errorf("failed to convert %s to script parameter", param)
} }
emit.AppCallNoArgs(script.BinWriter, contract, method, callflag.All) emit.AppCallNoArgs(script.BinWriter, contract, method, callflag.All)

View file

@ -26,9 +26,6 @@ func TestInvocationScriptCreationGood(t *testing.T) {
}, { }, {
ps: Params{{RawMessage: []byte(`42`)}}, ps: Params{{RawMessage: []byte(`42`)}},
script: "c21f0c0234320c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52", script: "c21f0c0234320c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52",
}, {
ps: Params{{RawMessage: []byte(`"m"`)}, {RawMessage: []byte(`true`)}},
script: "11db201f0c016d0c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52",
}, { }, {
ps: Params{{RawMessage: []byte(`"a"`)}, {RawMessage: []byte(`[]`)}}, ps: Params{{RawMessage: []byte(`"a"`)}, {RawMessage: []byte(`[]`)}},
script: "10c01f0c01610c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52", script: "10c01f0c01610c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52",
@ -72,7 +69,11 @@ func TestInvocationScriptCreationGood(t *testing.T) {
for i, ps := range paramScripts { for i, ps := range paramScripts {
method, err := ps.ps[0].GetString() method, err := ps.ps[0].GetString()
require.NoError(t, err, fmt.Sprintf("testcase #%d", i)) require.NoError(t, err, fmt.Sprintf("testcase #%d", i))
script, err := CreateFunctionInvocationScript(contract, method, ps.ps[1:]) var p *Param
if len(ps.ps) > 1 {
p = &ps.ps[1]
}
script, err := CreateFunctionInvocationScript(contract, method, p)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, ps.script, hex.EncodeToString(script), fmt.Sprintf("testcase #%d", i)) assert.Equal(t, ps.script, hex.EncodeToString(script), fmt.Sprintf("testcase #%d", i))
} }
@ -81,18 +82,19 @@ func TestInvocationScriptCreationGood(t *testing.T) {
func TestInvocationScriptCreationBad(t *testing.T) { func TestInvocationScriptCreationBad(t *testing.T) {
contract := util.Uint160{} contract := util.Uint160{}
var testParams = []Params{ var testParams = []Param{
{{RawMessage: []byte(`[{"type": "ByteArray", "value": "qwerty"}]`)}}, {RawMessage: []byte(`true`)},
{{RawMessage: []byte(`[{"type": "Signature", "value": "qwerty"}]`)}}, {RawMessage: []byte(`[{"type": "ByteArray", "value": "qwerty"}]`)},
{{RawMessage: []byte(`[{"type": "Hash160", "value": "qwerty"}]`)}}, {RawMessage: []byte(`[{"type": "Signature", "value": "qwerty"}]`)},
{{RawMessage: []byte(`[{"type": "Hash256", "value": "qwerty"}]`)}}, {RawMessage: []byte(`[{"type": "Hash160", "value": "qwerty"}]`)},
{{RawMessage: []byte(`[{"type": "PublicKey", "value": 42}]`)}}, {RawMessage: []byte(`[{"type": "Hash256", "value": "qwerty"}]`)},
{{RawMessage: []byte(`[{"type": "PublicKey", "value": "qwerty"}]`)}}, {RawMessage: []byte(`[{"type": "PublicKey", "value": 42}]`)},
{{RawMessage: []byte(`[{"type": "Integer", "value": "123q"}]`)}}, {RawMessage: []byte(`[{"type": "PublicKey", "value": "qwerty"}]`)},
{{RawMessage: []byte(`[{"type": "Unknown"}]`)}}, {RawMessage: []byte(`[{"type": "Integer", "value": "123q"}]`)},
{RawMessage: []byte(`[{"type": "Unknown"}]`)},
} }
for i, ps := range testParams { for i, ps := range testParams {
_, err := CreateFunctionInvocationScript(contract, "", ps) _, err := CreateFunctionInvocationScript(contract, "", &ps)
assert.NotNil(t, err, fmt.Sprintf("testcase #%d", i)) assert.NotNil(t, err, fmt.Sprintf("testcase #%d", i))
} }
} }

View file

@ -19,18 +19,30 @@ type Invoke struct {
Stack []stackitem.Item Stack []stackitem.Item
FaultException string FaultException string
Transaction *transaction.Transaction Transaction *transaction.Transaction
Diagnostics *InvokeDiag
maxIteratorResultItems int maxIteratorResultItems int
finalize func() finalize func()
} }
// InvokeDiag is an additional diagnostic data for invocation.
type InvokeDiag struct {
Invocations []*vm.InvocationTree `json:"invokedcontracts"`
}
// NewInvoke returns new Invoke structure with the given fields set. // NewInvoke returns new Invoke structure with the given fields set.
func NewInvoke(vm *vm.VM, finalize func(), script []byte, faultException string, maxIteratorResultItems int) *Invoke { func NewInvoke(vm *vm.VM, finalize func(), script []byte, faultException string, maxIteratorResultItems int) *Invoke {
var diag *InvokeDiag
tree := vm.GetInvocationTree()
if tree != nil {
diag = &InvokeDiag{Invocations: tree.Calls}
}
return &Invoke{ return &Invoke{
State: vm.State().String(), State: vm.State().String(),
GasConsumed: vm.GasConsumed(), GasConsumed: vm.GasConsumed(),
Script: script, Script: script,
Stack: vm.Estack().ToArray(), Stack: vm.Estack().ToArray(),
FaultException: faultException, FaultException: faultException,
Diagnostics: diag,
maxIteratorResultItems: maxIteratorResultItems, maxIteratorResultItems: maxIteratorResultItems,
finalize: finalize, finalize: finalize,
} }
@ -43,6 +55,7 @@ type invokeAux struct {
Stack json.RawMessage `json:"stack"` Stack json.RawMessage `json:"stack"`
FaultException string `json:"exception,omitempty"` FaultException string `json:"exception,omitempty"`
Transaction []byte `json:"tx,omitempty"` Transaction []byte `json:"tx,omitempty"`
Diagnostics *InvokeDiag `json:"diagnostics,omitempty"`
} }
type iteratorAux struct { type iteratorAux struct {
@ -121,6 +134,7 @@ func (r Invoke) MarshalJSON() ([]byte, error) {
Stack: st, Stack: st,
FaultException: r.FaultException, FaultException: r.FaultException,
Transaction: txbytes, Transaction: txbytes,
Diagnostics: r.Diagnostics,
}) })
} }
@ -176,5 +190,6 @@ func (r *Invoke) UnmarshalJSON(data []byte) error {
r.State = aux.State r.State = aux.State
r.FaultException = aux.FaultException r.FaultException = aux.FaultException
r.Transaction = tx r.Transaction = tx
r.Diagnostics = aux.Diagnostics
return nil return nil
} }

View file

@ -1554,33 +1554,45 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) {
// invokeFunction implements the `invokeFunction` RPC call. // invokeFunction implements the `invokeFunction` RPC call.
func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) { func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) {
if len(reqParams) < 2 {
return nil, response.ErrInvalidParams
}
scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0))
if responseErr != nil { if responseErr != nil {
return nil, responseErr return nil, responseErr
} }
tx := &transaction.Transaction{}
checkWitnessHashesIndex := len(reqParams)
if checkWitnessHashesIndex > 3 {
signers, _, err := reqParams[3].GetSignersWithWitnesses()
if err != nil {
return nil, response.ErrInvalidParams
}
tx.Signers = signers
checkWitnessHashesIndex--
}
if len(tx.Signers) == 0 {
tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}}
}
method, err := reqParams[1].GetString() method, err := reqParams[1].GetString()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, response.ErrInvalidParams
} }
script, err := request.CreateFunctionInvocationScript(scriptHash, method, reqParams[2:checkWitnessHashesIndex]) var params *request.Param
if len(reqParams) > 2 {
params = &reqParams[2]
}
tx := &transaction.Transaction{}
if len(reqParams) > 3 {
signers, _, err := reqParams[3].GetSignersWithWitnesses()
if err != nil {
return nil, response.ErrInvalidParams
}
tx.Signers = signers
}
var verbose bool
if len(reqParams) > 4 {
verbose, err = reqParams[4].GetBoolean()
if err != nil {
return nil, response.ErrInvalidParams
}
}
if len(tx.Signers) == 0 {
tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}}
}
script, err := request.CreateFunctionInvocationScript(scriptHash, method, params)
if err != nil { if err != nil {
return nil, response.NewInternalServerError("can't create invocation script", err) return nil, response.NewInternalServerError("can't create invocation script", err)
} }
tx.Script = script tx.Script = script
return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx) return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose)
} }
// invokescript implements the `invokescript` RPC call. // invokescript implements the `invokescript` RPC call.
@ -1603,11 +1615,18 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.
tx.Signers = signers tx.Signers = signers
tx.Scripts = witnesses tx.Scripts = witnesses
} }
var verbose bool
if len(reqParams) > 2 {
verbose, err = reqParams[2].GetBoolean()
if err != nil {
return nil, response.ErrInvalidParams
}
}
if len(tx.Signers) == 0 { if len(tx.Signers) == 0 {
tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}} tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}}
} }
tx.Script = script tx.Script = script
return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx) return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose)
} }
// invokeContractVerify implements the `invokecontractverify` RPC call. // invokeContractVerify implements the `invokecontractverify` RPC call.
@ -1644,7 +1663,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r
tx.Signers = []transaction.Signer{{Account: scriptHash}} tx.Signers = []transaction.Signer{{Account: scriptHash}}
tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}} tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}}
} }
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx) return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, false)
} }
func (s *Server) getFakeNextBlock() (*block.Block, error) { func (s *Server) getFakeNextBlock() (*block.Block, error) {
@ -1666,12 +1685,15 @@ func (s *Server) getFakeNextBlock() (*block.Block, error) {
// witness invocation script in case of `verification` trigger (it pushes `verify` // witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification // arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified. // contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction) (*result.Invoke, *response.Error) { func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, verbose bool) (*result.Invoke, *response.Error) {
b, err := s.getFakeNextBlock() b, err := s.getFakeNextBlock()
if err != nil { if err != nil {
return nil, response.NewInternalServerError("can't create fake block", err) return nil, response.NewInternalServerError("can't create fake block", err)
} }
vm, finalize := s.chain.GetTestVM(t, tx, b) vm, finalize := s.chain.GetTestVM(t, tx, b)
if verbose {
vm.EnableInvocationTree()
}
vm.GasLimit = int64(s.config.MaxGasInvoke) vm.GasLimit = int64(s.config.MaxGasInvoke)
if t == trigger.Verification { if t == trigger.Verification {
// We need this special case because witnesses verification is not the simple System.Contract.Call, // We need this special case because witnesses verification is not the simple System.Contract.Call,

View file

@ -22,8 +22,10 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -38,6 +40,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -877,6 +880,48 @@ var rpcTestCases = map[string][]rpcTestCase{
assert.NotEqual(t, 0, res.GasConsumed) assert.NotEqual(t, 0, res.GasConsumed)
}, },
}, },
{
name: "positive, verbose",
params: `["` + NNSHash.StringLE() + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`,
result: func(e *executor) interface{} {
script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0xdc, 0xe2, 0xd3, 0xba, 0xe, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x8, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a, 0x41, 0x62, 0x7d, 0x5b, 0x52}
stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib)
cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
return &result.Invoke{
State: "HALT",
GasConsumed: 17958510,
Script: script,
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Diagnostics: &result.InvokeDiag{
Invocations: []*vm.InvocationTree{{
Current: hash.Hash160(script),
Calls: []*vm.InvocationTree{
{
Current: NNSHash,
Calls: []*vm.InvocationTree{
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: cryptoHash,
},
},
},
},
}},
},
}
},
},
{ {
name: "no params", name: "no params",
params: `[]`, params: `[]`,
@ -911,6 +956,25 @@ var rpcTestCases = map[string][]rpcTestCase{
assert.NotEqual(t, 0, res.GasConsumed) assert.NotEqual(t, 0, res.GasConsumed)
}, },
}, },
{
name: "positive,verbose",
params: `["UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`,
result: func(e *executor) interface{} {
script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66}
return &result.Invoke{
State: "FAULT",
GasConsumed: 60,
Script: script,
Stack: []stackitem.Item{},
FaultException: "at instruction 0 (ROT): too big index",
Diagnostics: &result.InvokeDiag{
Invocations: []*vm.InvocationTree{{
Current: hash.Hash160(script),
}},
},
}
},
},
{ {
name: "positive, good witness", name: "positive, good witness",
// script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix // script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix

View file

@ -141,7 +141,7 @@ func (o *Oracle) testVerify(tx *transaction.Transaction) (int64, bool) {
v, finalize := o.Chain.GetTestVM(trigger.Verification, &cp, nil) v, finalize := o.Chain.GetTestVM(trigger.Verification, &cp, nil)
v.GasLimit = o.Chain.GetPolicer().GetMaxVerificationGAS() v.GasLimit = o.Chain.GetPolicer().GetMaxVerificationGAS()
v.LoadScriptWithHash(o.oracleScript, o.oracleHash, callflag.ReadOnly) v.LoadScriptWithHash(o.oracleScript, o.oracleHash, callflag.ReadOnly)
v.Jump(v.Context(), o.verifyOffset) v.Context().Jump(o.verifyOffset)
ok := isVerifyOk(v, finalize) ok := isVerifyOk(v, finalize)
return v.GasConsumed(), ok return v.GasConsumed(), ok

View file

@ -458,9 +458,9 @@ func handleRun(c *ishell.Context) {
c.Err(fmt.Errorf("no program loaded")) c.Err(fmt.Errorf("no program loaded"))
return return
} }
v.Jump(v.Context(), offset) v.Context().Jump(offset)
if initMD := m.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil { if initMD := m.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil {
v.Call(v.Context(), initMD.Offset) v.Call(initMD.Offset)
} }
} }
} }

View file

@ -48,42 +48,28 @@ type Context struct {
// Call flags this context was created with. // Call flags this context was created with.
callFlag callflag.CallFlag callFlag callflag.CallFlag
// ParamCount specifies number of parameters. // retCount specifies number of return values.
ParamCount int retCount int
// RetCount specifies number of return values.
RetCount int
// NEF represents NEF file for the current contract. // NEF represents NEF file for the current contract.
NEF *nef.File NEF *nef.File
// invTree is an invocation tree (or branch of it) for this context.
invTree *InvocationTree
} }
// CheckReturnState represents possible states of stack after opcode.RET was processed.
type CheckReturnState byte
const (
// NoCheck performs no return values check.
NoCheck CheckReturnState = 0
// EnsureIsEmpty checks that stack is empty and panics if not.
EnsureIsEmpty CheckReturnState = 1
// EnsureNotEmpty checks that stack contains not more than 1 element and panics if not.
// It pushes stackitem.Null on stack in case if there's no elements.
EnsureNotEmpty CheckReturnState = 2
)
var errNoInstParam = errors.New("failed to read instruction parameter") var errNoInstParam = errors.New("failed to read instruction parameter")
// NewContext returns a new Context object. // NewContext returns a new Context object.
func NewContext(b []byte) *Context { func NewContext(b []byte) *Context {
return NewContextWithParams(b, 0, -1, 0) return NewContextWithParams(b, -1, 0)
} }
// NewContextWithParams creates new Context objects using script, parameter count, // NewContextWithParams creates new Context objects using script, parameter count,
// return value count and initial position in script. // return value count and initial position in script.
func NewContextWithParams(b []byte, pcount int, rvcount int, pos int) *Context { func NewContextWithParams(b []byte, rvcount int, pos int) *Context {
return &Context{ return &Context{
prog: b, prog: b,
ParamCount: pcount, retCount: rvcount,
RetCount: rvcount, nextip: pos,
nextip: pos,
} }
} }
@ -97,6 +83,11 @@ func (c *Context) NextIP() int {
return c.nextip return c.nextip
} }
// Jump unconditionally moves the next instruction pointer to specified location.
func (c *Context) Jump(pos int) {
c.nextip = pos
}
// Next returns the next instruction to execute with its parameter if any. // Next returns the next instruction to execute with its parameter if any.
// The parameter is not copied and shouldn't be written to. After its invocation // The parameter is not copied and shouldn't be written to. After its invocation
// the instruction pointer points to the instruction being returned. // the instruction pointer points to the instruction being returned.

12
pkg/vm/invocation_tree.go Normal file
View file

@ -0,0 +1,12 @@
package vm
import (
"github.com/nspcc-dev/neo-go/pkg/util"
)
// InvocationTree represents a tree with script hashes, traversing it
// you can see how contracts called each other.
type InvocationTree struct {
Current util.Uint160 `json:"hash"`
Calls []*InvocationTree `json:"calls,omitempty"`
}

View file

@ -0,0 +1,69 @@
package vm
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/require"
)
func TestInvocationTree(t *testing.T) {
script := []byte{
byte(opcode.PUSH3), byte(opcode.DEC),
byte(opcode.DUP), byte(opcode.PUSH0), byte(opcode.JMPEQ), (2 + 2 + 2 + 6 + 1),
byte(opcode.CALL), (2 + 2), // CALL shouldn't affect invocation tree.
byte(opcode.JMP), 0xf9, // DEC
byte(opcode.SYSCALL), 0, 0, 0, 0, byte(opcode.DROP),
byte(opcode.RET),
byte(opcode.RET),
byte(opcode.PUSHINT8), 0xff,
}
cnt := 0
v := newTestVM()
v.SyscallHandler = func(v *VM, _ uint32) error {
if v.Istack().Len() > 4 { // top -> call -> syscall -> call -> syscall -> ...
v.Estack().PushVal(1)
return nil
}
cnt++
v.LoadScriptWithHash(script, util.Uint160{byte(cnt)}, 0)
return nil
}
v.EnableInvocationTree()
v.LoadScript(script)
topHash := v.Context().ScriptHash()
require.NoError(t, v.Run())
res := &InvocationTree{
Calls: []*InvocationTree{{
Current: topHash,
Calls: []*InvocationTree{
{
Current: util.Uint160{1},
Calls: []*InvocationTree{
{
Current: util.Uint160{2},
},
{
Current: util.Uint160{3},
},
},
},
{
Current: util.Uint160{4},
Calls: []*InvocationTree{
{
Current: util.Uint160{5},
},
{
Current: util.Uint160{6},
},
},
},
},
}},
}
require.Equal(t, res, v.GetInvocationTree())
}

View file

@ -86,8 +86,8 @@ type VM struct {
trigger trigger.Type trigger trigger.Type
// Invocations is a script invocation counter. // invTree is a top-level invocation tree (if enabled).
Invocations map[util.Uint160]int invTree *InvocationTree
} }
// New returns a new VM object ready to load AVM bytecode scripts. // New returns a new VM object ready to load AVM bytecode scripts.
@ -102,7 +102,6 @@ func NewWithTrigger(t trigger.Type) *VM {
trigger: t, trigger: t,
SyscallHandler: defaultSyscallHandler, SyscallHandler: defaultSyscallHandler,
Invocations: make(map[util.Uint160]int),
} }
initStack(&vm.istack, "invocation", nil) initStack(&vm.istack, "invocation", nil)
@ -137,16 +136,6 @@ func (v *VM) Istack() *Stack {
return &v.istack return &v.istack
} }
// LoadArgs loads in the arguments used in the Mian entry point.
func (v *VM) LoadArgs(method []byte, args []stackitem.Item) {
if len(args) > 0 {
v.estack.PushVal(args)
}
if method != nil {
v.estack.PushVal(method)
}
}
// PrintOps prints the opcodes of the current loaded program to stdout. // PrintOps prints the opcodes of the current loaded program to stdout.
func (v *VM) PrintOps(out io.Writer) { func (v *VM) PrintOps(out io.Writer) {
if out == nil { if out == nil {
@ -254,6 +243,16 @@ func (v *VM) LoadFileWithFlags(path string, f callflag.CallFlag) error {
return nil return nil
} }
// CollectInvocationTree enables collecting invocation tree data.
func (v *VM) EnableInvocationTree() {
v.invTree = &InvocationTree{}
}
// GetInvocationTree returns current invocation tree structure.
func (v *VM) GetInvocationTree() *InvocationTree {
return v.invTree
}
// 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) {
v.LoadWithFlags(prog, callflag.NoneFlag) v.LoadWithFlags(prog, callflag.NoneFlag)
@ -266,6 +265,7 @@ func (v *VM) LoadWithFlags(prog []byte, f callflag.CallFlag) {
v.estack.Clear() v.estack.Clear()
v.state = NoneState v.state = NoneState
v.gasConsumed = 0 v.gasConsumed = 0
v.invTree = nil
v.LoadScriptWithFlags(prog, f) v.LoadScriptWithFlags(prog, f)
} }
@ -278,15 +278,7 @@ func (v *VM) LoadScript(b []byte) {
// LoadScriptWithFlags loads script and sets call flag to f. // LoadScriptWithFlags loads script and sets call flag to f.
func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) { func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
v.checkInvocationStackSize() v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0)
ctx := NewContextWithParams(b, 0, -1, 0)
v.estack = newStack("evaluation", &v.refs)
ctx.estack = v.estack
initStack(&ctx.tryStack, "exception", nil)
ctx.callFlag = f
ctx.static = newSlot(&v.refs)
ctx.callingScriptHash = v.GetCurrentScriptHash()
v.istack.PushItem(ctx)
} }
// LoadScriptWithHash if similar to the LoadScriptWithFlags method, but it also loads // LoadScriptWithHash if similar to the LoadScriptWithFlags method, but it also loads
@ -296,24 +288,49 @@ func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
// accordingly). It's up to user of this function to make sure the script and hash match // accordingly). It's up to user of this function to make sure the script and hash match
// each other. // each other.
func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) { func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) {
shash := v.GetCurrentScriptHash() v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), hash, f, 1, 0)
v.LoadScriptWithCallingHash(shash, b, hash, f, true, 0)
} }
// LoadScriptWithCallingHash is similar to LoadScriptWithHash but sets calling hash explicitly. // LoadNEFMethod allows to create a context to execute a method from the NEF
// file with specified caller and executing hash, call flags, return value,
// method and _initialize offsets.
func (v *VM) LoadNEFMethod(exe *nef.File, caller util.Uint160, hash util.Uint160, f callflag.CallFlag,
hasReturn bool, methodOff int, initOff int) {
var rvcount int
if hasReturn {
rvcount = 1
}
v.loadScriptWithCallingHash(exe.Script, exe, caller, hash, f, rvcount, methodOff)
if initOff >= 0 {
v.Call(initOff)
}
}
// loadScriptWithCallingHash is similar to LoadScriptWithHash but sets calling hash explicitly.
// It should be used for calling from native contracts. // It should be used for calling from native contracts.
func (v *VM) LoadScriptWithCallingHash(caller util.Uint160, b []byte, hash util.Uint160, func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, caller util.Uint160,
f callflag.CallFlag, hasReturn bool, paramCount uint16) { hash util.Uint160, f callflag.CallFlag, rvcount int, offset int) {
v.LoadScriptWithFlags(b, f) v.checkInvocationStackSize()
ctx := v.Context() ctx := NewContextWithParams(b, rvcount, offset)
v.estack = newStack("evaluation", &v.refs)
ctx.estack = v.estack
initStack(&ctx.tryStack, "exception", nil)
ctx.callFlag = f
ctx.static = newSlot(&v.refs)
ctx.scriptHash = hash ctx.scriptHash = hash
ctx.callingScriptHash = caller ctx.callingScriptHash = caller
if hasReturn { ctx.NEF = exe
ctx.RetCount = 1 if v.invTree != nil {
} else { curTree := v.invTree
ctx.RetCount = 0 parent := v.Context()
if parent != nil {
curTree = parent.invTree
}
newTree := &InvocationTree{Current: ctx.ScriptHash()}
curTree.Calls = append(curTree.Calls, newTree)
ctx.invTree = newTree
} }
ctx.ParamCount = int(paramCount) v.istack.PushItem(ctx)
} }
// Context returns the current executed context. Nil if there is no context, // Context returns the current executed context. Nil if there is no context,
@ -1321,7 +1338,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
} }
if cond { if cond {
v.Jump(ctx, offset) ctx.Jump(offset)
} }
case opcode.CALL, opcode.CALLL: case opcode.CALL, opcode.CALLL:
@ -1362,9 +1379,9 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
newEstack := v.Context().estack newEstack := v.Context().estack
if oldEstack != newEstack { if oldEstack != newEstack {
if oldCtx.RetCount >= 0 && oldEstack.Len() != oldCtx.RetCount { if oldCtx.retCount >= 0 && oldEstack.Len() != oldCtx.retCount {
panic(fmt.Errorf("invalid return values count: expected %d, got %d", panic(fmt.Errorf("invalid return values count: expected %d, got %d",
oldCtx.RetCount, oldEstack.Len())) oldCtx.retCount, oldEstack.Len()))
} }
rvcount := oldEstack.Len() rvcount := oldEstack.Len()
for i := rvcount; i > 0; i-- { for i := rvcount; i > 0; i-- {
@ -1492,7 +1509,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
} else { } else {
ctx.tryStack.Pop() ctx.tryStack.Pop()
} }
v.Jump(ctx, eOffset) ctx.Jump(eOffset)
case opcode.ENDFINALLY: case opcode.ENDFINALLY:
if v.uncaughtException != nil { if v.uncaughtException != nil {
@ -1500,7 +1517,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
return return
} }
eCtx := ctx.tryStack.Pop().Value().(*exceptionHandlingContext) eCtx := ctx.tryStack.Pop().Value().(*exceptionHandlingContext)
v.Jump(ctx, eCtx.EndOffset) ctx.Jump(eCtx.EndOffset)
default: default:
panic(fmt.Sprintf("unknown opcode %s", op.String())) panic(fmt.Sprintf("unknown opcode %s", op.String()))
@ -1556,17 +1573,9 @@ func (v *VM) throw(item stackitem.Item) {
v.handleException() v.handleException()
} }
// Jump performs jump to the offset. // Call calls method by offset using new execution context.
func (v *VM) Jump(ctx *Context, offset int) { func (v *VM) Call(offset int) {
ctx.nextip = offset v.call(v.Context(), offset)
}
// Call calls method by offset. It is similar to Jump but also
// pushes new context to the invocation stack and increments
// invocation counter for the corresponding context script hash.
func (v *VM) Call(ctx *Context, offset int) {
v.call(ctx, offset)
v.Invocations[ctx.ScriptHash()]++
} }
// call is an internal representation of Call, which does not // call is an internal representation of Call, which does not
@ -1575,13 +1584,13 @@ func (v *VM) Call(ctx *Context, offset int) {
func (v *VM) call(ctx *Context, offset int) { func (v *VM) call(ctx *Context, offset int) {
v.checkInvocationStackSize() v.checkInvocationStackSize()
newCtx := ctx.Copy() newCtx := ctx.Copy()
newCtx.RetCount = -1 newCtx.retCount = -1
newCtx.local = nil newCtx.local = nil
newCtx.arguments = nil newCtx.arguments = nil
initStack(&newCtx.tryStack, "exception", nil) initStack(&newCtx.tryStack, "exception", nil)
newCtx.NEF = ctx.NEF newCtx.NEF = ctx.NEF
v.istack.PushItem(newCtx) v.istack.PushItem(newCtx)
v.Jump(newCtx, offset) newCtx.Jump(offset)
} }
// getJumpOffset returns instruction number in a current context // getJumpOffset returns instruction number in a current context
@ -1637,10 +1646,10 @@ func (v *VM) handleException() {
ectx.State = eCatch ectx.State = eCatch
v.estack.PushItem(v.uncaughtException) v.estack.PushItem(v.uncaughtException)
v.uncaughtException = nil v.uncaughtException = nil
v.Jump(ictx, ectx.CatchOffset) ictx.Jump(ectx.CatchOffset)
} else { } else {
ectx.State = eFinally ectx.State = eFinally
v.Jump(ictx, ectx.FinallyOffset) ictx.Jump(ectx.FinallyOffset)
} }
return return
} }