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{}) {
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)
}
}

View file

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

View file

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

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

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.
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

View file

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

View file

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

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.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()

View file

@ -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 {
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 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)
return nil, fmt.Errorf("failed to convert %s to script parameter", param)
}
emit.AppCallNoArgs(script.BinWriter, contract, method, callflag.All)

View file

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

View file

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

View file

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

View file

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

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.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

View file

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

View file

@ -48,41 +48,27 @@ 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,
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.

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
// 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
}
ctx.ParamCount = int(paramCount)
newTree := &InvocationTree{Current: ctx.ScriptHash()}
curTree.Calls = append(curTree.Calls, newTree)
ctx.invTree = newTree
}
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
}