interop/contract: fix state rollbacks for nested contexts

Our wrapping optimization relied on the caller context having a TRY block,
but each context (including internal calls!) has an exception handling stack
of its own, which means that for an invocation stack of

    entry
    A.someMethodFromEntry()   # this one has a TRY
    A.internalMethodViaCALL() # this one doesn't
    B.someMethod()

we get `HasTryBlock() == false` for `A.internalMethodViaCALL()` context, which
leads to missing wrapper and missing rollbacks if B is to THROW. What this
patch does instead is it checks for any context within contract boundaries.

Fixes #3045.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
This commit is contained in:
Roman Khimov 2023-06-29 11:18:30 +03:00
parent 50c8805034
commit 70aed34d77
4 changed files with 26 additions and 11 deletions

View file

@ -117,7 +117,7 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra
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.
wrapped := ic.VM.ContractHasTryBlock() && // 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

View file

@ -304,6 +304,9 @@ func TestSnapshotIsolation_Exceptions(t *testing.T) {
for i := 0; i < nNtfB1; i++ {
runtime.Notify("NotificationFromB before panic", i)
}
internalCaller(keyA, valueA, nNtfA)
}
func internalCaller(keyA, valueA []byte, nNtfA int) {
contract.Call(interop.Hash160{` + hashAStr + `}, "doAndPanic", contract.All, keyA, valueA, nNtfA)
}
func CheckStorageChanges() bool {

View file

@ -315,16 +315,6 @@ func (v *VM) PushContextScriptHash(n int) error {
return nil
}
func (c *Context) HasTryBlock() bool {
for i := 0; i < c.tryStack.Len(); i++ {
eCtx := c.tryStack.Peek(i).Value().(*exceptionHandlingContext)
if eCtx.State == eTry {
return true
}
}
return false
}
// MarshalJSON implements the JSON marshalling interface.
func (c *Context) MarshalJSON() ([]byte, error) {
var aux = contextAux{

View file

@ -1804,6 +1804,28 @@ func throwUnhandledException(item stackitem.Item) {
panic(msg)
}
// ContractHasTryBlock checks if the currently executing contract has a TRY
// block in one of its contexts.
func (v *VM) ContractHasTryBlock() bool {
var topctx *Context // Currently executing context.
for i := 0; i < len(v.istack); i++ {
ictx := v.istack[len(v.istack)-1-i] // It's a stack, going backwards like handleException().
if topctx == nil {
topctx = ictx
}
if ictx.sc != topctx.sc {
return false // Different contract -> no one cares.
}
for j := 0; j < ictx.tryStack.Len(); j++ {
eCtx := ictx.tryStack.Peek(j).Value().(*exceptionHandlingContext)
if eCtx.State == eTry {
return true
}
}
}
return false
}
// CheckMultisigPar checks if the sigs contains sufficient valid signatures.
func CheckMultisigPar(v *VM, curve elliptic.Curve, h []byte, pkeys [][]byte, sigs [][]byte) bool {
if len(sigs) == 1 {