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 eea14c92d..76cf975c5 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1577,6 +1577,13 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons } 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}} } @@ -1585,7 +1592,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons 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. @@ -1608,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. @@ -1649,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) { @@ -1671,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/vm/invocation_tree.go b/pkg/vm/invocation_tree.go index b4daff18a..dec64d11a 100644 --- a/pkg/vm/invocation_tree.go +++ b/pkg/vm/invocation_tree.go @@ -7,6 +7,6 @@ import ( // InvocationTree represents a tree with script hashes, traversing it // you can see how contracts called each other. type InvocationTree struct { - Current util.Uint160 - Calls []*InvocationTree + Current util.Uint160 `json:"hash"` + Calls []*InvocationTree `json:"calls,omitempty"` }