diff --git a/pkg/compiler/vm_test.go b/pkg/compiler/vm_test.go index 694975b86..73eed9e48 100644 --- a/pkg/compiler/vm_test.go +++ b/pkg/compiler/vm_test.go @@ -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{}) { 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() require.NoError(t, err) 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) v.LoadScriptWithFlags(script, callflag.All) - v.Jump(v.Context(), mainOffset) + v.Context().Jump(mainOffset) if initOffset >= 0 { - v.Call(v.Context(), initOffset) + v.Call(initOffset) } } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 041888e3a..fd96656b7 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2213,14 +2213,14 @@ func (bc *Blockchain) InitVerificationVM(v *vm.VM, getContract func(util.Uint160 if md == nil || md.ReturnType != smartcontract.BoolType { return ErrInvalidVerificationContract } - initMD := cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0) - v.LoadScriptWithHash(cs.NEF.Script, hash, callflag.ReadOnly) - v.Context().NEF = &cs.NEF - v.Jump(v.Context(), md.Offset) - - if initMD != nil { - v.Call(v.Context(), initMD.Offset) + verifyOffset := md.Offset + initOffset := -1 + md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0) + if md != nil { + initOffset = md.Offset } + v.LoadNEFMethod(&cs.NEF, util.Uint160{}, hash, callflag.ReadOnly, + true, verifyOffset, initOffset) } if len(witness.InvocationScript) != 0 { err := vm.IsScriptCorrect(witness.InvocationScript, nil) diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index ce6ba1550..daa202b94 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -48,6 +48,7 @@ type Context struct { Log *zap.Logger VM *vm.VM Functions []Function + Invocations map[util.Uint160]int cancelFuncs []context.CancelFunc getContract func(dao.DAO, util.Uint160) (*state.Contract, error) baseExecFee int64 @@ -73,6 +74,7 @@ func NewContext(trigger trigger.Type, bc blockchainer.Blockchainer, d dao.DAO, Tx: tx, DAO: dao, Log: log, + Invocations: make(map[util.Uint160]int), getContract: getContract, baseExecFee: baseExecFee, } diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 8389e4557..7daf13073 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -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)) } - ic.VM.Invocations[cs.Hash]++ - ic.VM.LoadScriptWithCallingHash(caller, cs.NEF.Script, cs.Hash, ic.VM.Context().GetCallFlags()&f, hasReturn, uint16(len(args))) - 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 - } - + methodOff := md.Offset + initOff := -1 md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0) 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 } diff --git a/pkg/core/interop/runtime/util.go b/pkg/core/interop/runtime/util.go index d14684e66..efc844b35 100644 --- a/pkg/core/interop/runtime/util.go +++ b/pkg/core/interop/runtime/util.go @@ -63,10 +63,10 @@ func GetNotifications(ic *interop.Context) error { // GetInvocationCounter returns how many times current contract was invoked during current tx execution. func GetInvocationCounter(ic *interop.Context) error { currentScriptHash := ic.VM.GetCurrentScriptHash() - count, ok := ic.VM.Invocations[currentScriptHash] + count, ok := ic.Invocations[currentScriptHash] if !ok { count = 1 - ic.VM.Invocations[currentScriptHash] = count + ic.Invocations[currentScriptHash] = count } ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(count)))) return nil diff --git a/pkg/core/interop/runtime/util_test.go b/pkg/core/interop/runtime/util_test.go index 4827498c4..88b1fbdd2 100644 --- a/pkg/core/interop/runtime/util_test.go +++ b/pkg/core/interop/runtime/util_test.go @@ -97,9 +97,9 @@ func TestRuntimeGetNotifications(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() - ic.VM.Invocations[h] = 42 + ic.Invocations[h] = 42 t.Run("No invocations", func(t *testing.T) { h1 := h diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 4ee1c63da..06e75656d 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -229,21 +229,31 @@ func TestRuntimeGetNotifications(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) { - v.LoadScript([]byte{1}) + v.Load([]byte{1}) // do not return an error in this case. require.NoError(t, runtime.GetInvocationCounter(ic)) require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) }) t.Run("NonZero", func(t *testing.T) { - v.LoadScript([]byte{2}) + v.Load([]byte{2}) require.NoError(t, runtime.GetInvocationCounter(ic)) 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) { @@ -756,6 +766,9 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) { burnGasOff := w.Len() emit.Syscall(w.BinWriter, interopnames.SystemRuntimeBurnGas) emit.Opcodes(w.BinWriter, opcode.RET) + invocCounterOff := w.Len() + emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetInvocationCounter) + emit.Opcodes(w.BinWriter, opcode.RET) script := w.Bytes() h := hash.Hash160(script) @@ -925,6 +938,11 @@ func getTestContractState(bc *Blockchain) (*state.Contract, *state.Contract) { }, ReturnType: smartcontract.VoidType, }, + { + Name: "invocCounter", + Offset: invocCounterOff, + ReturnType: smartcontract.IntegerType, + }, } m.Permissions = make([]manifest.Permission, 2) m.Permissions[0].Contract.Type = manifest.PermissionHash diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index b9b652b63..803c5abba 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -243,7 +243,7 @@ func TestNativeContract_InvokeInternal(t *testing.T) { v.LoadScriptWithHash(tn.Metadata().NEF.Script, util.Uint160{1, 2, 3}, callflag.All) v.Estack().PushVal(14) v.Estack().PushVal(28) - v.Jump(v.Context(), sumOffset) + v.Context().Jump(sumOffset) // it's prohibited to call natives directly 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.Estack().PushVal(14) v.Estack().PushVal(28) - v.Jump(v.Context(), sumOffset) + v.Context().Jump(sumOffset) // it's prohibited to call natives before NativeUpdateHistory[0] height 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.Estack().PushVal(14) v.Estack().PushVal(28) - v.Jump(v.Context(), sumOffset) + v.Context().Jump(sumOffset) require.NoError(t, v.Run()) value := v.Estack().Pop().BigInt() diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index c61a8f4c7..4a18af412 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -3,7 +3,6 @@ package request import ( "errors" "fmt" - "strconv" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "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 // 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() - for i := len(params) - 1; i >= 0; i-- { - 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 { + if param == nil { 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) diff --git a/pkg/rpc/request/tx_builder_test.go b/pkg/rpc/request/tx_builder_test.go index 52a36627b..1cc837fe6 100644 --- a/pkg/rpc/request/tx_builder_test.go +++ b/pkg/rpc/request/tx_builder_test.go @@ -26,9 +26,6 @@ func TestInvocationScriptCreationGood(t *testing.T) { }, { ps: Params{{RawMessage: []byte(`42`)}}, script: "c21f0c0234320c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52", - }, { - ps: Params{{RawMessage: []byte(`"m"`)}, {RawMessage: []byte(`true`)}}, - script: "11db201f0c016d0c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52", }, { ps: Params{{RawMessage: []byte(`"a"`)}, {RawMessage: []byte(`[]`)}}, script: "10c01f0c01610c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52", @@ -72,7 +69,11 @@ func TestInvocationScriptCreationGood(t *testing.T) { for i, ps := range paramScripts { method, err := ps.ps[0].GetString() 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.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) { contract := util.Uint160{} - var testParams = []Params{ - {{RawMessage: []byte(`[{"type": "ByteArray", "value": "qwerty"}]`)}}, - {{RawMessage: []byte(`[{"type": "Signature", "value": "qwerty"}]`)}}, - {{RawMessage: []byte(`[{"type": "Hash160", "value": "qwerty"}]`)}}, - {{RawMessage: []byte(`[{"type": "Hash256", "value": "qwerty"}]`)}}, - {{RawMessage: []byte(`[{"type": "PublicKey", "value": 42}]`)}}, - {{RawMessage: []byte(`[{"type": "PublicKey", "value": "qwerty"}]`)}}, - {{RawMessage: []byte(`[{"type": "Integer", "value": "123q"}]`)}}, - {{RawMessage: []byte(`[{"type": "Unknown"}]`)}}, + var testParams = []Param{ + {RawMessage: []byte(`true`)}, + {RawMessage: []byte(`[{"type": "ByteArray", "value": "qwerty"}]`)}, + {RawMessage: []byte(`[{"type": "Signature", "value": "qwerty"}]`)}, + {RawMessage: []byte(`[{"type": "Hash160", "value": "qwerty"}]`)}, + {RawMessage: []byte(`[{"type": "Hash256", "value": "qwerty"}]`)}, + {RawMessage: []byte(`[{"type": "PublicKey", "value": 42}]`)}, + {RawMessage: []byte(`[{"type": "PublicKey", "value": "qwerty"}]`)}, + {RawMessage: []byte(`[{"type": "Integer", "value": "123q"}]`)}, + {RawMessage: []byte(`[{"type": "Unknown"}]`)}, } for i, ps := range testParams { - _, err := CreateFunctionInvocationScript(contract, "", ps) + _, err := CreateFunctionInvocationScript(contract, "", &ps) assert.NotNil(t, err, fmt.Sprintf("testcase #%d", i)) } } diff --git a/pkg/rpc/response/result/invoke.go b/pkg/rpc/response/result/invoke.go index ebd7b699c..f8b5b5cc6 100644 --- a/pkg/rpc/response/result/invoke.go +++ b/pkg/rpc/response/result/invoke.go @@ -19,18 +19,30 @@ type Invoke struct { Stack []stackitem.Item FaultException string Transaction *transaction.Transaction + Diagnostics *InvokeDiag maxIteratorResultItems int 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. 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{ State: vm.State().String(), GasConsumed: vm.GasConsumed(), Script: script, Stack: vm.Estack().ToArray(), FaultException: faultException, + Diagnostics: diag, maxIteratorResultItems: maxIteratorResultItems, finalize: finalize, } @@ -43,6 +55,7 @@ type invokeAux struct { Stack json.RawMessage `json:"stack"` FaultException string `json:"exception,omitempty"` Transaction []byte `json:"tx,omitempty"` + Diagnostics *InvokeDiag `json:"diagnostics,omitempty"` } type iteratorAux struct { @@ -121,6 +134,7 @@ func (r Invoke) MarshalJSON() ([]byte, error) { Stack: st, FaultException: r.FaultException, Transaction: txbytes, + Diagnostics: r.Diagnostics, }) } @@ -176,5 +190,6 @@ func (r *Invoke) UnmarshalJSON(data []byte) error { r.State = aux.State r.FaultException = aux.FaultException r.Transaction = tx + r.Diagnostics = aux.Diagnostics return nil } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 81bf5f6ca..76cf975c5 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1554,33 +1554,45 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) { // invokeFunction implements the `invokeFunction` RPC call. 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)) if responseErr != nil { 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() if err != nil { 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 { return nil, response.NewInternalServerError("can't create invocation script", err) } 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. @@ -1603,11 +1615,18 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response. tx.Signers = signers 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 { tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}} } 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. @@ -1644,7 +1663,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r tx.Signers = []transaction.Signer{{Account: scriptHash}} 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) { @@ -1666,12 +1685,15 @@ func (s *Server) getFakeNextBlock() (*block.Block, error) { // witness invocation script in case of `verification` trigger (it pushes `verify` // arguments on stack before verification). In case of contract verification // 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() if err != nil { return nil, response.NewInternalServerError("can't create fake block", err) } vm, finalize := s.chain.GetTestVM(t, tx, b) + if verbose { + vm.EnableInvocationTree() + } vm.GasLimit = int64(s.config.MaxGasInvoke) if t == trigger.Verification { // We need this special case because witnesses verification is not the simple System.Contract.Call, diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index a380157f4..a073db5ad 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -22,8 +22,10 @@ import ( "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/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/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/encoding/address" "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/emit" "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/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -877,6 +880,48 @@ var rpcTestCases = map[string][]rpcTestCase{ 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", params: `[]`, @@ -911,6 +956,25 @@ var rpcTestCases = map[string][]rpcTestCase{ 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", // script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix diff --git a/pkg/services/oracle/response.go b/pkg/services/oracle/response.go index f188778d5..2c4be8262 100644 --- a/pkg/services/oracle/response.go +++ b/pkg/services/oracle/response.go @@ -141,7 +141,7 @@ func (o *Oracle) testVerify(tx *transaction.Transaction) (int64, bool) { v, finalize := o.Chain.GetTestVM(trigger.Verification, &cp, nil) v.GasLimit = o.Chain.GetPolicer().GetMaxVerificationGAS() v.LoadScriptWithHash(o.oracleScript, o.oracleHash, callflag.ReadOnly) - v.Jump(v.Context(), o.verifyOffset) + v.Context().Jump(o.verifyOffset) ok := isVerifyOk(v, finalize) return v.GasConsumed(), ok diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 6069bba92..c3b39e02a 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -458,9 +458,9 @@ func handleRun(c *ishell.Context) { c.Err(fmt.Errorf("no program loaded")) return } - v.Jump(v.Context(), offset) + v.Context().Jump(offset) if initMD := m.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil { - v.Call(v.Context(), initMD.Offset) + v.Call(initMD.Offset) } } } diff --git a/pkg/vm/context.go b/pkg/vm/context.go index fc5388e02..1eaba0ce0 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -48,42 +48,28 @@ type Context struct { // Call flags this context was created with. callFlag callflag.CallFlag - // ParamCount specifies number of parameters. - ParamCount int - // RetCount specifies number of return values. - RetCount int + // retCount specifies number of return values. + retCount int // NEF represents NEF file for the current contract. 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") // NewContext returns a new Context object. 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, // 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{ - prog: b, - ParamCount: pcount, - RetCount: rvcount, - nextip: pos, + prog: b, + retCount: rvcount, + nextip: pos, } } @@ -97,6 +83,11 @@ func (c *Context) NextIP() int { 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. // The parameter is not copied and shouldn't be written to. After its invocation // the instruction pointer points to the instruction being returned. diff --git a/pkg/vm/invocation_tree.go b/pkg/vm/invocation_tree.go new file mode 100644 index 000000000..dec64d11a --- /dev/null +++ b/pkg/vm/invocation_tree.go @@ -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"` +} diff --git a/pkg/vm/invocation_tree_test.go b/pkg/vm/invocation_tree_test.go new file mode 100644 index 000000000..dc60ba677 --- /dev/null +++ b/pkg/vm/invocation_tree_test.go @@ -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()) +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 63a06981b..960b0e4f7 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -86,8 +86,8 @@ type VM struct { trigger trigger.Type - // Invocations is a script invocation counter. - Invocations map[util.Uint160]int + // invTree is a top-level invocation tree (if enabled). + invTree *InvocationTree } // New returns a new VM object ready to load AVM bytecode scripts. @@ -102,7 +102,6 @@ func NewWithTrigger(t trigger.Type) *VM { trigger: t, SyscallHandler: defaultSyscallHandler, - Invocations: make(map[util.Uint160]int), } initStack(&vm.istack, "invocation", nil) @@ -137,16 +136,6 @@ func (v *VM) Istack() *Stack { 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. func (v *VM) PrintOps(out io.Writer) { if out == nil { @@ -254,6 +243,16 @@ func (v *VM) LoadFileWithFlags(path string, f callflag.CallFlag) error { 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. func (v *VM) Load(prog []byte) { v.LoadWithFlags(prog, callflag.NoneFlag) @@ -266,6 +265,7 @@ func (v *VM) LoadWithFlags(prog []byte, f callflag.CallFlag) { v.estack.Clear() v.state = NoneState v.gasConsumed = 0 + v.invTree = nil v.LoadScriptWithFlags(prog, f) } @@ -278,15 +278,7 @@ func (v *VM) LoadScript(b []byte) { // LoadScriptWithFlags loads script and sets call flag to f. func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) { - v.checkInvocationStackSize() - 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) + v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0) } // 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 // each other. func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) { - shash := v.GetCurrentScriptHash() - v.LoadScriptWithCallingHash(shash, b, hash, f, true, 0) + v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), hash, f, 1, 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. -func (v *VM) LoadScriptWithCallingHash(caller util.Uint160, b []byte, hash util.Uint160, - f callflag.CallFlag, hasReturn bool, paramCount uint16) { - v.LoadScriptWithFlags(b, f) - ctx := v.Context() +func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, caller util.Uint160, + hash util.Uint160, f callflag.CallFlag, rvcount int, offset int) { + v.checkInvocationStackSize() + 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.callingScriptHash = caller - if hasReturn { - ctx.RetCount = 1 - } else { - ctx.RetCount = 0 + ctx.NEF = exe + if v.invTree != nil { + curTree := v.invTree + 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, @@ -1321,7 +1338,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro } if cond { - v.Jump(ctx, offset) + ctx.Jump(offset) } 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 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", - oldCtx.RetCount, oldEstack.Len())) + oldCtx.retCount, oldEstack.Len())) } rvcount := oldEstack.Len() for i := rvcount; i > 0; i-- { @@ -1492,7 +1509,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro } else { ctx.tryStack.Pop() } - v.Jump(ctx, eOffset) + ctx.Jump(eOffset) case opcode.ENDFINALLY: if v.uncaughtException != nil { @@ -1500,7 +1517,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro return } eCtx := ctx.tryStack.Pop().Value().(*exceptionHandlingContext) - v.Jump(ctx, eCtx.EndOffset) + ctx.Jump(eCtx.EndOffset) default: panic(fmt.Sprintf("unknown opcode %s", op.String())) @@ -1556,17 +1573,9 @@ func (v *VM) throw(item stackitem.Item) { v.handleException() } -// Jump performs jump to the offset. -func (v *VM) Jump(ctx *Context, offset int) { - ctx.nextip = 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 calls method by offset using new execution context. +func (v *VM) Call(offset int) { + v.call(v.Context(), offset) } // 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) { v.checkInvocationStackSize() newCtx := ctx.Copy() - newCtx.RetCount = -1 + newCtx.retCount = -1 newCtx.local = nil newCtx.arguments = nil initStack(&newCtx.tryStack, "exception", nil) newCtx.NEF = ctx.NEF v.istack.PushItem(newCtx) - v.Jump(newCtx, offset) + newCtx.Jump(offset) } // getJumpOffset returns instruction number in a current context @@ -1637,10 +1646,10 @@ func (v *VM) handleException() { ectx.State = eCatch v.estack.PushItem(v.uncaughtException) v.uncaughtException = nil - v.Jump(ictx, ectx.CatchOffset) + ictx.Jump(ectx.CatchOffset) } else { ectx.State = eFinally - v.Jump(ictx, ectx.FinallyOffset) + ictx.Jump(ectx.FinallyOffset) } return }