neo-go/pkg/core/interop/contract/call.go
Roman Khimov e8d2277fe5 contract/vm: only push NULL after call in dynamic contexts
And determine the need for Null dynamically. For some reason the only dynamic
context is Contract.Call. CALLT is not dynamic and neither is a call from
native contract, go figure...
2022-08-05 14:35:00 +03:00

186 lines
6.2 KiB
Go

package contract
import (
"errors"
"fmt"
"math/big"
"strings"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
type policyChecker interface {
IsBlocked(*dao.Simple, util.Uint160) bool
}
// LoadToken calls method specified by the token id.
func LoadToken(ic *interop.Context, id int32) error {
ctx := ic.VM.Context()
if !ctx.GetCallFlags().Has(callflag.ReadStates | callflag.AllowCall) {
return errors.New("invalid call flags")
}
tok := ctx.GetNEF().Tokens[id]
if int(tok.ParamCount) > ctx.Estack().Len() {
return errors.New("stack is too small")
}
args := make([]stackitem.Item, tok.ParamCount)
for i := range args {
args[i] = ic.VM.Estack().Pop().Item()
}
cs, err := ic.GetContract(tok.Hash)
if err != nil {
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
}
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args, false)
}
// Call calls a contract with flags.
func Call(ic *interop.Context) error {
h := ic.VM.Estack().Pop().Bytes()
method := ic.VM.Estack().Pop().String()
fs := callflag.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64()))
if fs&^callflag.All != 0 {
return errors.New("call flags out of range")
}
args := ic.VM.Estack().Pop().Array()
u, err := util.Uint160DecodeBytesBE(h)
if err != nil {
return errors.New("invalid contract hash")
}
cs, err := ic.GetContract(u)
if err != nil {
return fmt.Errorf("called contract %s not found: %w", u.StringLE(), err)
}
if strings.HasPrefix(method, "_") {
return errors.New("invalid method name (starts with '_')")
}
md := cs.Manifest.ABI.GetMethod(method, len(args))
if md == nil {
return fmt.Errorf("method not found: %s/%d", method, len(args))
}
hasReturn := md.ReturnType != smartcontract.VoidType
return callInternal(ic, cs, method, fs, hasReturn, args, true)
}
func callInternal(ic *interop.Context, cs *state.Contract, name string, f callflag.CallFlag,
hasReturn bool, args []stackitem.Item, isDynamic bool) error {
md := cs.Manifest.ABI.GetMethod(name, len(args))
if md.Safe {
f &^= (callflag.WriteStates | callflag.AllowNotify)
} else if ctx := ic.VM.Context(); ctx != nil && ctx.IsDeployed() {
curr, err := ic.GetContract(ic.VM.GetCurrentScriptHash())
if err == nil {
if !curr.Manifest.CanCall(cs.Hash, &cs.Manifest, name) {
return errors.New("disallowed method call")
}
}
}
return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn, isDynamic, false)
}
// callExFromNative calls a contract with flags using the provided calling hash.
func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract,
name string, args []stackitem.Item, f callflag.CallFlag, hasReturn bool, isDynamic bool, callFromNative bool) error {
for _, nc := range ic.Natives {
if nc.Metadata().Name == nativenames.Policy {
var pch = nc.(policyChecker)
if pch.IsBlocked(ic.DAO, cs.Hash) {
return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE())
}
break
}
}
md := cs.Manifest.ABI.GetMethod(name, len(args))
if md == nil {
return fmt.Errorf("method '%s' not found", name)
}
if len(args) != len(md.Parameters) {
return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters))
}
methodOff := md.Offset
initOff := -1
md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0)
if md != nil {
initOff = md.Offset
}
ic.Invocations[cs.Hash]++
f = ic.VM.Context().GetCallFlags() & f
wrapped := ic.VM.Context().HasTryBlock() && // If the method is not wrapped into try-catch block, then changes should be discarded anyway if exception occurs.
f&(callflag.All^callflag.ReadOnly) != 0 // If the method is safe, then it's read-only and doesn't perform storage changes or emit notifications.
baseNtfCount := len(ic.Notifications)
baseDAO := ic.DAO
if wrapped {
ic.DAO = ic.DAO.GetPrivate()
}
onUnload := func(ctx *vm.Context, commit bool) error {
if wrapped {
if commit {
_, err := ic.DAO.Persist()
if err != nil {
return fmt.Errorf("failed to persist changes %w", err)
}
} else {
ic.Notifications = ic.Notifications[:baseNtfCount] // Rollback all notification changes made by current context.
}
ic.DAO = baseDAO
}
if isDynamic && commit {
eLen := ctx.Estack().Len()
if eLen == 0 && ctx.NumOfReturnVals() == 0 { // No return value and none expected.
ic.VM.Context().Estack().PushItem(stackitem.Null{}) // Must use current context stack.
} else if eLen > 1 { // 1 or -1 (all) retrun values expected, but only one can be returned.
return errors.New("multiple return values in a cross-contract call")
} // All other rvcount/stack length mismatches are checked by the VM.
}
if callFromNative && !commit {
return fmt.Errorf("unhandled exception")
}
return nil
}
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, f,
hasReturn, methodOff, initOff, onUnload)
for e, i := ic.VM.Estack(), len(args)-1; i >= 0; i-- {
e.PushItem(args[i])
}
return nil
}
// ErrNativeCall is returned for failed calls from native.
var ErrNativeCall = errors.New("failed native call")
// CallFromNative performs synchronous call from native contract.
func CallFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract, method string, args []stackitem.Item, hasReturn bool) error {
startSize := ic.VM.Istack().Len()
if err := callExFromNative(ic, caller, cs, method, args, callflag.All, hasReturn, false, true); err != nil {
return err
}
for !ic.VM.HasStopped() && ic.VM.Istack().Len() > startSize {
if err := ic.VM.Step(); err != nil {
return fmt.Errorf("%w: %v", ErrNativeCall, err)
}
}
if ic.VM.HasFailed() {
return ErrNativeCall
}
return nil
}
// GetCallFlags returns current context calling flags.
func GetCallFlags(ic *interop.Context) error {
ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(ic.VM.Context().GetCallFlags()))))
return nil
}