diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index d9ff39426..987f881cf 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -35,6 +35,9 @@ const ( DefaultBaseExecFee = 30 // ContextNonceDataLen is a length of [Context.NonceData] in bytes. ContextNonceDataLen = 16 + // MaxNotificationCount is the maximum number of notifications per a single + // application execution. + MaxNotificationCount = 512 ) // Ledger is the interface to Blockchain required for Context functionality. @@ -564,10 +567,18 @@ func (ic *Context) IsHardforkActivation(hf config.Hardfork) bool { } // AddNotification creates notification event and appends it to the notification list. -func (ic *Context) AddNotification(hash util.Uint160, name string, item *stackitem.Array) { +func (ic *Context) AddNotification(hash util.Uint160, name string, item *stackitem.Array) error { + if ic.IsHardforkEnabled(config.HFEchidna) { + // Do not check persisting triggers to avoid native persist failure. Do not check + // verification trigger since verification context is loaded with ReadOnly flag. + if ic.Trigger == trigger.Application && len(ic.Notifications) == MaxNotificationCount { + return fmt.Errorf("notification count shouldn't exceed %d", MaxNotificationCount) + } + } ic.Notifications = append(ic.Notifications, state.NotificationEvent{ ScriptHash: hash, Name: name, Item: item, }) + return nil } diff --git a/pkg/core/interop/runtime/engine.go b/pkg/core/interop/runtime/engine.go index 3a16a4055..87ea6bb7b 100644 --- a/pkg/core/interop/runtime/engine.go +++ b/pkg/core/interop/runtime/engine.go @@ -114,8 +114,7 @@ func Notify(ic *interop.Context) error { if len(bytes) > MaxNotificationSize { return fmt.Errorf("notification size shouldn't exceed %d", MaxNotificationSize) } - ic.AddNotification(curHash, name, stackitem.DeepCopy(stackitem.NewArray(args), true).(*stackitem.Array)) - return nil + return ic.AddNotification(curHash, name, stackitem.DeepCopy(stackitem.NewArray(args), true).(*stackitem.Array)) } // LoadScript takes a script and arguments from the stack and loads it into the VM. diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index de6a7b9be..612f6c1ca 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -412,11 +412,10 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs return fmt.Errorf("failed to update Designation role data cache: %w", err) } - ic.AddNotification(s.Hash, DesignationEventName, stackitem.NewArray([]stackitem.Item{ + return ic.AddNotification(s.Hash, DesignationEventName, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(r))), stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))), })) - return nil } func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) { diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 6fb384cbf..54640a866 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -372,7 +372,10 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) panic(err) } m.callDeploy(ic, newcontract, args[2], false) - m.emitNotification(ic, contractDeployNotificationName, newcontract.Hash) + err = m.emitNotification(ic, contractDeployNotificationName, newcontract.Hash) + if err != nil { + panic(err) + } return contractToStack(newcontract) } @@ -444,7 +447,10 @@ func (m *Management) updateWithData(ic *interop.Context, args []stackitem.Item) panic(err) } m.callDeploy(ic, contract, args[2], true) - m.emitNotification(ic, contractUpdateNotificationName, contract.Hash) + err = m.emitNotification(ic, contractUpdateNotificationName, contract.Hash) + if err != nil { + panic(err) + } return stackitem.Null{} } @@ -497,7 +503,10 @@ func (m *Management) destroy(ic *interop.Context, sis []stackitem.Item) stackite if err != nil { panic(err) } - m.emitNotification(ic, contractDestroyNotificationName, hash) + err = m.emitNotification(ic, contractDestroyNotificationName, hash) + if err != nil { + panic(err) + } return stackitem.Null{} } @@ -681,7 +690,10 @@ func (m *Management) OnPersist(ic *interop.Context) error { if isUpdate { ntfName = contractUpdateNotificationName } - m.emitNotification(ic, ntfName, cs.Hash) + err = m.emitNotification(ic, ntfName, cs.Hash) + if err != nil { + return err + } } return nil @@ -806,8 +818,8 @@ func (m *Management) getNextContractID(d *dao.Simple) (int32, error) { return ret, nil } -func (m *Management) emitNotification(ic *interop.Context, name string, hash util.Uint160) { - ic.AddNotification(m.Hash, name, stackitem.NewArray([]stackitem.Item{addrToStackItem(&hash)})) +func (m *Management) emitNotification(ic *interop.Context, name string, hash util.Uint160) error { + return ic.AddNotification(m.Hash, name, stackitem.NewArray([]stackitem.Item{addrToStackItem(&hash)})) } func checkScriptAndMethods(ic *interop.Context, script []byte, methods []manifest.Method) error { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 9978e0b97..f99e6583e 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -459,9 +459,12 @@ func (n *NEO) OnPersist(ic *interop.Context) error { ic.DAO.PutStorageItem(n.ID, prefixCommittee, cache.committee.Bytes(ic.DAO.GetItemCtx())) if oldCommittee != nil { - ic.AddNotification(n.Hash, "CommitteeChanged", stackitem.NewArray([]stackitem.Item{ + err := ic.AddNotification(n.Hash, "CommitteeChanged", stackitem.NewArray([]stackitem.Item{ oldCommittee, newCommittee, })) + if err != nil { + return err + } } } return nil @@ -828,7 +831,8 @@ func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stac return stackitem.NewBool(err == nil) } -// RegisterCandidateInternal registers pub as a new candidate. +// RegisterCandidateInternal registers pub as a new candidate. This method must not be +// called outside of VM since it panics on critical errors. func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { var emitEvent = true @@ -843,16 +847,23 @@ func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey c.Registered = true } err := putConvertibleToDAO(n.ID, ic.DAO, key, c) + if err != nil { + return err + } if emitEvent { cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) cache.votesChanged = true - ic.AddNotification(n.Hash, "CandidateStateChanged", stackitem.NewArray([]stackitem.Item{ + err = ic.AddNotification(n.Hash, "CandidateStateChanged", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(pub.Bytes()), stackitem.NewBool(c.Registered), stackitem.NewBigInteger(&c.Votes), })) + if err != nil { + // Panic since it's a critical error that must abort execution. + panic(err) + } } - return err + return nil } func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -867,7 +878,8 @@ func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) st return stackitem.NewBool(err == nil) } -// UnregisterCandidateInternal unregisters pub as a candidate. +// UnregisterCandidateInternal unregisters pub as a candidate. This method must not be +// called outside of VM since it panics on critical errors. func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { var err error @@ -887,14 +899,21 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK if !ok { err = putConvertibleToDAO(n.ID, ic.DAO, key, c) } + if err != nil { + return err + } if emitEvent { - ic.AddNotification(n.Hash, "CandidateStateChanged", stackitem.NewArray([]stackitem.Item{ + err := ic.AddNotification(n.Hash, "CandidateStateChanged", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(pub.Bytes()), stackitem.NewBool(c.Registered), stackitem.NewBigInteger(&c.Votes), })) + if err != nil { + // Panic since it's a critical error that must abort execution. + panic(err) + } } - return err + return nil } func (n *NEO) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -907,7 +926,8 @@ func (n *NEO) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { return stackitem.NewBool(err == nil) } -// VoteInternal votes from account h for validarors specified in pubs. +// VoteInternal votes from account h for validarors specified in pubs. This method +// must not be called outside of VM since it panics on critical errors. func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.PublicKey) error { ok, err := runtime.CheckHashedWitness(ic, h) if err != nil { @@ -969,12 +989,16 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public } ic.DAO.PutStorageItem(n.ID, key, acc.Bytes(ic.DAO.GetItemCtx())) - ic.AddNotification(n.Hash, "Vote", stackitem.NewArray([]stackitem.Item{ + err = ic.AddNotification(n.Hash, "Vote", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(h.BytesBE()), keyToStackItem(oldVote), keyToStackItem(pub), stackitem.NewBigInteger(&acc.Balance), })) + if err != nil { + // Panic since it's a critical error that must abort execution. + panic(err) + } if newGas != nil { // Can be if it was already distributed in the same block. n.GAS.mint(ic, h, newGas, true) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index c179fb568..f23c97465 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -143,7 +143,11 @@ func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint } } }() - c.emitTransfer(ic, from, to, amount) + err := c.emitTransfer(ic, from, to, amount) + if err != nil { + skipPostCalls = true + panic(err) + } if to == nil || !callOnPayment { return } @@ -167,8 +171,8 @@ func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint } } -func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) { - ic.AddNotification(c.Hash, "Transfer", stackitem.NewArray([]stackitem.Item{ +func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) error { + return ic.AddNotification(c.Hash, "Transfer", stackitem.NewArray([]stackitem.Item{ addrToStackItem(from), addrToStackItem(to), stackitem.NewBigInteger(amount), diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 3f4b08aca..5c5a9dbe2 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -316,10 +316,13 @@ func (o *Oracle) FinishInternal(ic *interop.Context) error { if err != nil { return ErrRequestNotFound } - ic.AddNotification(o.Hash, "OracleResponse", stackitem.NewArray([]stackitem.Item{ + err = ic.AddNotification(o.Hash, "OracleResponse", stackitem.NewArray([]stackitem.Item{ stackitem.Make(resp.ID), stackitem.Make(req.OriginalTxID.BytesBE()), })) + if err != nil { + return err + } origTx, _, err := ic.DAO.GetTransaction(req.OriginalTxID) if err != nil { @@ -422,12 +425,15 @@ func (o *Oracle) RequestInternal(ic *interop.Context, url string, filter *string } else { filterNotif = stackitem.Null{} } - ic.AddNotification(o.Hash, "OracleRequest", stackitem.NewArray([]stackitem.Item{ + err = ic.AddNotification(o.Hash, "OracleRequest", stackitem.NewArray([]stackitem.Item{ stackitem.Make(id), stackitem.Make(ic.VM.GetCallingScriptHash().BytesBE()), stackitem.Make(url), filterNotif, })) + if err != nil { + return err + } req := &state.OracleRequest{ OriginalTxID: o.getOriginalTxID(ic.DAO, ic.Tx), GasForResponse: gas.Uint64(),