diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index 5f70bdf378..37c7a87a52 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -2620,3 +2620,78 @@ func TestBlockchain_StoreAsTransaction_ExecutableConflict(t *testing.T) { require.NoError(t, err) require.Equal(t, 2, len(aer)) } + +// TestEngineLimits ensures that MaxStackSize limit is preserved during System.Runtime.GetNotifications +// syscall handling. This test is an adjusted port of https://github.com/lazynode/Tanya/pull/33 and makes +// sure that NeoGo node is not affected by https://github.com/neo-project/neo/issues/3300 and does not need +// the https://github.com/neo-project/neo/pull/3301. +func TestEngineLimits(t *testing.T) { + bc, acc := chain.NewSingle(t) + e := neotest.NewExecutor(t, bc, acc, acc) + + src := `package test + import ( + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + ) + // args is an array of LargeEvent parameters containing 500 empty strings. + var args = []any{"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; + func ProduceNumerousNotifications(count int) [][]any { + for i := 0; i < count; i++ { + runtime.Notify("LargeEvent", args...) + } + return runtime.GetNotifications(runtime.GetExecutingScriptHash()) + } + func ProduceLargeObject(count int) int { + for i := 0; i < count; i++ { + runtime.Notify("LargeEvent", args...) + } + var ( + smallObject = make([]int, 100) + res []int + ) + for i := 0; i < count; i++ { + runtime.GetNotifications(runtime.GetExecutingScriptHash()) + res = append(res, smallObject...) + } + return len(res) + }` + const eArgsCount = 500 + eParams := make([]compiler.HybridParameter, eArgsCount) + for i := range eParams { + eParams[i].Name = fmt.Sprintf("str%d", i) + eParams[i].Type = smartcontract.ByteArrayType + } + c := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(src), &compiler.Options{ + Name: "test_contract", + ContractEvents: []compiler.HybridEvent{ + { + Name: "LargeEvent", + Parameters: eParams, + }, + }, + }) + e.DeployContract(t, c, nil) + + // ProduceNumerousNotifications: 1 iteration, no limits are hit. + var args = make([]stackitem.Item, eArgsCount) + for i := range args { + args[i] = stackitem.Make("") + } + cInv := e.NewInvoker(c.Hash, acc) + cInv.Invoke(t, stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(c.Hash), + stackitem.Make("LargeEvent"), + stackitem.Make(args), + }), + }), "produceNumerousNotifications", 1) + + // ProduceNumerousNotifications: hit the limit. + cInv.InvokeFail(t, "stack is too big", "produceNumerousNotifications", 500) + + // ProduceLargeObject: 1 iteration, no limits are hit. + cInv.Invoke(t, 100*1, "produceLargeObject", 1) + + // ProduceLargeObject: hit the limit. + cInv.InvokeFail(t, "stack is too big", "produceLargeObject", 500) +} diff --git a/pkg/vm/stackitem/item.go b/pkg/vm/stackitem/item.go index 33ff664bf7..6eacbea18b 100644 --- a/pkg/vm/stackitem/item.go +++ b/pkg/vm/stackitem/item.go @@ -128,6 +128,12 @@ func Make(v any) Item { a = append(a, Make(i)) } return Make(a) + case []string: + var a []Item + for _, i := range val { + a = append(a, Make(i)) + } + return Make(a) case []any: res := make([]Item, len(val)) for i := range val { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index d3959f1e2b..2b94cbcb7d 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -594,7 +594,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro err = newError(ctx.ip, op, errRecover) } else if v.refs > MaxStackSize { v.state = vmstate.Fault - err = newError(ctx.ip, op, "stack is too big") + err = newError(ctx.ip, op, fmt.Sprintf("stack is too big: %d vs %d", int(v.refs), MaxStackSize)) } }() @@ -1995,7 +1995,7 @@ func validateMapKey(key Element) { func (v *VM) checkInvocationStackSize() { if len(v.istack) >= MaxInvocationStackSize { - panic("invocation stack is too big") + panic(fmt.Sprintf("invocation stack is too big: %d", len(v.istack))) } }