Skip to content

Commit

Permalink
runtime: implement System.Runtime.LoadScript, fix #2701
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-khimov committed Sep 29, 2022
1 parent 3dbd36e commit 27769ae
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 11 deletions.
13 changes: 4 additions & 9 deletions pkg/core/interop/contract/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra
if wrapped {
ic.DAO = ic.DAO.GetPrivate()
}
onUnload := func(ctx *vm.Context, commit bool) error {
onUnload := func(v *vm.VM, ctx *vm.Context, commit bool) error {
if wrapped {
if commit {
_, err := ic.DAO.Persist()
Expand All @@ -136,17 +136,12 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra
}
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")
}
if isDynamic {
return vm.DynamicOnUnload(v, ctx, commit)
}
return nil
}
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, f,
Expand Down
1 change: 1 addition & 0 deletions pkg/core/interop/interopnames/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
SystemRuntimeGetScriptContainer = "System.Runtime.GetScriptContainer"
SystemRuntimeGetTime = "System.Runtime.GetTime"
SystemRuntimeGetTrigger = "System.Runtime.GetTrigger"
SystemRuntimeLoadScript = "System.Runtime.LoadScript"
SystemRuntimeLog = "System.Runtime.Log"
SystemRuntimeNotify = "System.Runtime.Notify"
SystemRuntimePlatform = "System.Runtime.Platform"
Expand Down
18 changes: 18 additions & 0 deletions pkg/core/interop/runtime/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math/big"

"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -91,6 +92,23 @@ func Notify(ic *interop.Context) error {
return nil
}

// LoadScript takes a script and arguments from the stack and loads it into the VM.
func LoadScript(ic *interop.Context) error {
script := ic.VM.Estack().Pop().Bytes()
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()
fs = ic.VM.Context().GetCallFlags() & callflag.ReadOnly & fs
ic.VM.LoadDynamicScript(script, fs)

for e, i := ic.VM.Estack(), len(args)-1; i >= 0; i-- {
e.PushItem(args[i])
}
return nil
}

// Log logs the message passed.
func Log(ic *interop.Context) error {
state := ic.VM.Estack().Pop().String()
Expand Down
78 changes: 78 additions & 0 deletions pkg/core/interop/runtime/ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Ui
ic.VM.GasLimit = -1
}

func wrapDynamicScript(t *testing.T, script []byte, flags callflag.CallFlag, args ...interface{}) []byte {
b := io.NewBufBinWriter()

// Params.
emit.Array(b.BinWriter, args...)
emit.Int(b.BinWriter, int64(flags))
emit.Bytes(b.BinWriter, script)

// Wrapped syscall.
emit.Instruction(b.BinWriter, opcode.TRY, []byte{3 + 5 + 2, 0}) // 3
emit.Syscall(b.BinWriter, interopnames.SystemRuntimeLoadScript) // 5
emit.Instruction(b.BinWriter, opcode.ENDTRY, []byte{1 + 11 + 2}) // 2

// Catch block
emit.Opcodes(b.BinWriter, opcode.DROP)
emit.String(b.BinWriter, "exception") // 1 + 1 + 9 == 11 bytes
emit.Opcodes(b.BinWriter, opcode.RET)

require.NoError(t, b.Err)
return b.Bytes()
}

func TestBurnGas(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
Expand Down Expand Up @@ -370,6 +392,62 @@ func TestCheckWitness(t *testing.T) {
})
}

func TestLoadScript(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)

t.Run("no ret val", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.RET)}, callflag.All)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Null{})
})
t.Run("empty script", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{}, callflag.All)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Null{})
})
t.Run("ret val, no params", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.PUSH1)}, callflag.All)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make(1))
})
t.Run("ret val with params", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.MUL)}, callflag.All, 2, 2)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make(4))
})
t.Run("two retrun values", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.PUSH1), byte(opcode.PUSH1)}, callflag.All, 2, 2)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "multiple return values in a cross-contract call")
})
t.Run("invalid flags", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.MUL)}, callflag.CallFlag(0xff), 2, 2)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "call flags out of range")
})
t.Run("abort", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.ABORT)}, callflag.All)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "ABORT")
})
t.Run("internal call", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(e.NativeHash(t, nativenames.Gas), "decimals")
require.NoError(t, err)
script = wrapDynamicScript(t, script, callflag.ReadOnly)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make(8))
})
t.Run("forbidden internal call", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(e.NativeHash(t, nativenames.Neo), "decimals")
require.NoError(t, err)
script = wrapDynamicScript(t, script, callflag.ReadStates)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "missing call flags")
})
t.Run("internal state-changing call", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(e.NativeHash(t, nativenames.Neo), "transfer", acc.ScriptHash(), acc.ScriptHash(), 1, nil)
require.NoError(t, err)
script = wrapDynamicScript(t, script, callflag.All)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "missing call flags")
})
t.Run("exception", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.PUSH1), byte(opcode.THROW)}, callflag.ReadOnly)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make("exception"))
})
}

func TestGasLeft(t *testing.T) {
const runtimeGasLeftPrice = 1 << 4

Expand Down
2 changes: 2 additions & 0 deletions pkg/core/interops.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ var systemInterops = []interop.Function{
{Name: interopnames.SystemRuntimeGetScriptContainer, Func: runtime.GetScriptContainer, Price: 1 << 3},
{Name: interopnames.SystemRuntimeGetTime, Func: runtime.GetTime, Price: 1 << 3, RequiredFlags: callflag.ReadStates},
{Name: interopnames.SystemRuntimeGetTrigger, Func: runtime.GetTrigger, Price: 1 << 3},
{Name: interopnames.SystemRuntimeLoadScript, Func: runtime.LoadScript, Price: 1 << 15, RequiredFlags: callflag.AllowCall,
ParamCount: 3},
{Name: interopnames.SystemRuntimeLog, Func: runtime.Log, Price: 1 << 15, RequiredFlags: callflag.AllowNotify,
ParamCount: 1},
{Name: interopnames.SystemRuntimeNotify, Func: runtime.Notify, Price: 1 << 15, RequiredFlags: callflag.AllowNotify,
Expand Down
21 changes: 20 additions & 1 deletion pkg/vm/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ type Context struct {
}

// ContextUnloadCallback is a callback method used on context unloading from istack.
type ContextUnloadCallback func(ctx *Context, commit bool) error
type ContextUnloadCallback func(v *VM, ctx *Context, commit bool) error

var errNoInstParam = errors.New("failed to read instruction parameter")

// ErrMultiRet is returned when caller does not expect multiple return values
// from callee.
var ErrMultiRet = errors.New("multiple return values in a cross-contract call")

// NewContext returns a new Context object.
func NewContext(b []byte) *Context {
return NewContextWithParams(b, -1, 0)
Expand Down Expand Up @@ -357,3 +361,18 @@ func (c *Context) HasTryBlock() bool {
}
return false
}

// DynamicOnUnload implements OnUnload script for dynamic calls, if no exception
// has occured it checks that the context has exactly 0 (in which case a `Null`
// is pushed) or 1 returned value.
func DynamicOnUnload(v *VM, ctx *Context, commit bool) error {
if commit {
eLen := ctx.Estack().Len()
if eLen == 0 { // No return value, add one.
v.Context().Estack().PushItem(stackitem.Null{}) // Must use current context stack.
} else if eLen > 1 { // Only one can be returned.
return errors.New("multiple return values in a cross-contract call")
} // One value returned, it's OK.
}
return nil
}
9 changes: 8 additions & 1 deletion pkg/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, nil)
}

// LoadDynamicScript loads the given script with the given flags. This script is
// considered to be dynamic, it can either return no value at all or return
// exactly one value.
func (v *VM) LoadDynamicScript(b []byte, f callflag.CallFlag) {
v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, DynamicOnUnload)
}

// LoadScriptWithHash is similar to the LoadScriptWithFlags method, but it also loads
// the given script hash directly into the Context to avoid its recalculations and to make
// it possible to override it for deployed contracts with special hashes (the function
Expand Down Expand Up @@ -1634,7 +1641,7 @@ func (v *VM) unloadContext(ctx *Context) {
ctx.sc.static.ClearRefs(&v.refs)
}
if ctx.sc.onUnload != nil {
err := ctx.sc.onUnload(ctx, v.uncaughtException == nil)
err := ctx.sc.onUnload(v, ctx, v.uncaughtException == nil)
if err != nil {
errMessage := fmt.Sprintf("context unload callback failed: %s", err)
if v.uncaughtException != nil {
Expand Down

0 comments on commit 27769ae

Please sign in to comment.