diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index dcc4cf6d1..231b7bc59 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -55,6 +55,10 @@ var ( Name: "out", Usage: "file to put JSON transaction to", } + forceFlag = cli.StringFlag{ + Name: "force", + Usage: "force-push the transaction in case of bad VM state after test script invocation", + } ) const ( @@ -110,6 +114,7 @@ func NewCommands() []cli.Command { addressFlag, gasFlag, outFlag, + forceFlag, } invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...) return []cli.Command{{ @@ -487,6 +492,13 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if err != nil { return cli.NewExitError(err, 1) } + if signAndPush && resp.State != "HALT" { + errText := fmt.Sprintf("Warning: %s VM state returned from the RPC node: %s\n", resp.State, resp.FaultException) + if ctx.String("force") == "" { + return cli.NewExitError(errText+". Use --force flag to send the transaction anyway.", 1) + } + fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...") + } if out := ctx.String("out"); out != "" { script, err := hex.DecodeString(resp.Script) if err != nil { diff --git a/examples/runtime/runtime.go b/examples/runtime/runtime.go index 027d3b441..27de617fd 100644 --- a/examples/runtime/runtime.go +++ b/examples/runtime/runtime.go @@ -16,6 +16,14 @@ func init() { trigger = runtime.GetTrigger() } +func _deploy(isUpdate bool) { + if isUpdate { + Log("_deploy method called before contract update") + return + } + Log("_deploy method called before contract creation") +} + // CheckWitness checks owner's witness func CheckWitness() bool { // Log owner upon Verification trigger diff --git a/examples/timer/timer.go b/examples/timer/timer.go new file mode 100644 index 000000000..2fcabd03c --- /dev/null +++ b/examples/timer/timer.go @@ -0,0 +1,96 @@ +package timer + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/engine" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" +) + +const defaultTicks = 3 + +var ( + // ctx holds storage context for contract methods + ctx storage.Context + // Check if the invoker of the contract is the specified owner + owner = util.FromAddress("NULwe3UAHckN2fzNdcVg31tDiaYtMDwANt") + // ticksKey is a storage key for ticks counter + ticksKey = []byte("ticks") +) + +func init() { + ctx = storage.GetContext() +} + +func _deploy(isUpdate bool) { + if isUpdate { + ticksLeft := storage.Get(ctx, ticksKey).(int) + 1 + storage.Put(ctx, ticksKey, ticksLeft) + runtime.Log("One more tick is added.") + return + } + storage.Put(ctx, ticksKey, defaultTicks) + runtime.Log("Timer set to " + itoa(defaultTicks) + " ticks.") +} + +// Migrate migrates the contract. +func Migrate(script []byte, manifest []byte) bool { + if !runtime.CheckWitness(owner) { + runtime.Log("Only owner is allowed to update the contract.") + return false + } + contract.Update(script, manifest) + runtime.Log("Contract updated.") + return true +} + +// Tick decrement ticks count and checks whether the timer is fired. +func Tick() bool { + runtime.Log("Tick-tock.") + ticksLeft := storage.Get(ctx, ticksKey) + ticksLeft = ticksLeft.(int) - 1 + if ticksLeft == 0 { + runtime.Log("Fired!") + return engine.AppCall(runtime.GetExecutingScriptHash(), "selfDestroy").(bool) + } + storage.Put(ctx, ticksKey, ticksLeft) + runtime.Log(itoa(ticksLeft.(int)) + " ticks left.") + return true +} + +// SelfDestroy destroys the contract. +func SelfDestroy() bool { + if !(runtime.CheckWitness(owner) || runtime.CheckWitness(runtime.GetExecutingScriptHash())) { + runtime.Log("Only owner or the contract itself are allowed to destroy the contract.") + return false + } + contract.Destroy() + runtime.Log("Destroyed.") + return true +} + +// itoa converts int to string +func itoa(i int) string { + digits := "0123456789" + var ( + res string + isNegative bool + ) + if i < 0 { + i = -i + isNegative = true + } + for { + r := i % 10 + res = digits[r:r+1] + res + i = i / 10 + if i == 0 { + break + } + } + if isNegative { + res = "-" + res + } + return res +} diff --git a/examples/timer/timer.yml b/examples/timer/timer.yml new file mode 100644 index 000000000..d9b6984cf --- /dev/null +++ b/examples/timer/timer.yml @@ -0,0 +1,4 @@ +hasstorage: true +ispayable: false +supportedstandards: [] +events: [] diff --git a/pkg/compiler/func_scope.go b/pkg/compiler/func_scope.go index 3e977c42e..0b72c0aed 100644 --- a/pkg/compiler/func_scope.go +++ b/pkg/compiler/func_scope.go @@ -105,6 +105,11 @@ func (c *funcScope) analyzeVoidCalls(node ast.Node) bool { return false } } + case *ast.TypeAssertExpr: + ce, ok := n.X.(*ast.CallExpr) + if ok { + c.voidCalls[ce] = false + } case *ast.BinaryExpr: return false case *ast.RangeStmt: @@ -112,6 +117,11 @@ func (c *funcScope) analyzeVoidCalls(node ast.Node) bool { if ok { c.voidCalls[ce] = false } + case *ast.UnaryExpr: + ce, ok := n.X.(*ast.CallExpr) + if ok { + c.voidCalls[ce] = false + } case *ast.IfStmt: // we can't just return `false`, because we still need to process body ce, ok := n.Cond.(*ast.CallExpr) diff --git a/pkg/compiler/if_test.go b/pkg/compiler/if_test.go index 038c8fe92..5ee1c420e 100644 --- a/pkg/compiler/if_test.go +++ b/pkg/compiler/if_test.go @@ -121,3 +121,32 @@ func TestInitIF(t *testing.T) { }) }) } + +func TestCallExpIF(t *testing.T) { + t.Run("Call", func(t *testing.T) { + src := `package foo + func someFunc() bool { + return true + } + func Main() int { + if someFunc() { + return 5 + } + return 6 + }` + eval(t, src, big.NewInt(5)) + }) + t.Run("CallWithUnaryExpression", func(t *testing.T) { + src := `package foo + func someFunc() bool { + return false + } + func Main() int { + if !someFunc() { + return 5 + } + return 6 + }` + eval(t, src, big.NewInt(5)) + }) +} diff --git a/pkg/compiler/return_test.go b/pkg/compiler/return_test.go index 53f33dca8..1a822c3f0 100644 --- a/pkg/compiler/return_test.go +++ b/pkg/compiler/return_test.go @@ -123,3 +123,18 @@ func TestNamedReturn(t *testing.T) { t.Run("EmptyReturn", runCase("", big.NewInt(1), big.NewInt(2))) t.Run("AnotherVariable", runCase("b, c", big.NewInt(2), big.NewInt(3))) } + +func TestTypeAssertReturn(t *testing.T) { + src := ` + package main + + func foo() interface{} { + return 5 + } + + func Main() int { + return foo().(int) + } + ` + eval(t, src, big.NewInt(5)) +} diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index e27a54894..4d757049e 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract" "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" ) @@ -51,12 +52,12 @@ func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem return errors.New("disallowed method call") } } - return CallExInternal(ic, cs, name, args, f) + return CallExInternal(ic, cs, name, args, f, vm.EnsureNotEmpty) } // CallExInternal calls a contract with flags and can't be invoked directly by user. func CallExInternal(ic *interop.Context, cs *state.Contract, - name string, args []stackitem.Item, f smartcontract.CallFlag) error { + name string, args []stackitem.Item, f smartcontract.CallFlag, checkReturn vm.CheckReturnState) error { md := cs.Manifest.ABI.GetMethod(name) if md == nil { return fmt.Errorf("method '%s' not found", name) @@ -86,7 +87,7 @@ func CallExInternal(ic *interop.Context, cs *state.Contract, // use Jump not Call here because context was loaded in LoadScript above. ic.VM.Jump(ic.VM.Context(), md.Offset) } - ic.VM.Context().CheckReturn = true + ic.VM.Context().CheckReturn = checkReturn md = cs.Manifest.ABI.GetMethod(manifest.MethodInit) if md != nil { diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index f59276306..e1a8be374 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -203,7 +203,7 @@ func callDeploy(ic *interop.Context, cs *state.Contract, isUpdate bool) error { md := cs.Manifest.ABI.GetMethod(manifest.MethodDeploy) if md != nil { return contract.CallExInternal(ic, cs, manifest.MethodDeploy, - []stackitem.Item{stackitem.NewBool(isUpdate)}, smartcontract.All) + []stackitem.Item{stackitem.NewBool(isUpdate)}, smartcontract.All, vm.EnsureIsEmpty) } return nil } diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 7abdfcbd7..6afa5eb31 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -912,7 +912,7 @@ func TestContractCreateDeploy(t *testing.T) { require.NoError(t, ic.VM.Run()) v.LoadScriptWithFlags(currCs.Script, smartcontract.All) - err := contract.CallExInternal(ic, cs, "getValue", nil, smartcontract.All) + err := contract.CallExInternal(ic, cs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty) require.NoError(t, err) require.NoError(t, v.Run()) require.Equal(t, "create", v.Estack().Pop().String()) @@ -933,7 +933,7 @@ func TestContractCreateDeploy(t *testing.T) { require.NoError(t, v.Run()) v.LoadScriptWithFlags(currCs.Script, smartcontract.All) - err = contract.CallExInternal(ic, newCs, "getValue", nil, smartcontract.All) + err = contract.CallExInternal(ic, newCs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty) require.NoError(t, err) require.NoError(t, v.Run()) require.Equal(t, "update", v.Estack().Pop().String()) diff --git a/pkg/vm/context.go b/pkg/vm/context.go index ce36ff783..10da72c0a 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -46,9 +46,22 @@ type Context struct { callFlag smartcontract.CallFlag // CheckReturn specifies if amount of return values needs to be checked. - CheckReturn bool + CheckReturn CheckReturnState } +// 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. diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 0a18f3b59..9d22a96f1 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -1409,7 +1409,13 @@ func (v *VM) unloadContext(ctx *Context) { if ctx.static != nil && currCtx != nil && ctx.static != currCtx.static { ctx.static.Clear() } - if ctx.CheckReturn { + switch ctx.CheckReturn { + case NoCheck: + case EnsureIsEmpty: + if currCtx != nil && ctx.estack.len != 0 { + panic("return value amount is > 0") + } + case EnsureNotEmpty: if currCtx != nil && ctx.estack.len == 0 { currCtx.estack.PushVal(stackitem.Null{}) } else if ctx.estack.len > 1 { @@ -1471,7 +1477,7 @@ func (v *VM) Call(ctx *Context, offset int) { // package. func (v *VM) call(ctx *Context, offset int) { newCtx := ctx.Copy() - newCtx.CheckReturn = false + newCtx.CheckReturn = NoCheck newCtx.local = nil newCtx.arguments = nil newCtx.tryStack = NewStack("exception")