github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/core/interop/contract/call_test.go (about)

     1  package contract_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math/big"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/nspcc-dev/neo-go/internal/contracts"
    12  	"github.com/nspcc-dev/neo-go/internal/random"
    13  	"github.com/nspcc-dev/neo-go/pkg/compiler"
    14  	"github.com/nspcc-dev/neo-go/pkg/core/block"
    15  	"github.com/nspcc-dev/neo-go/pkg/core/interop"
    16  	"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
    17  	"github.com/nspcc-dev/neo-go/pkg/core/native"
    18  	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
    19  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    20  	"github.com/nspcc-dev/neo-go/pkg/neotest"
    21  	"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
    22  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    23  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
    24  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
    25  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
    26  	"github.com/nspcc-dev/neo-go/pkg/util"
    27  	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
    28  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts")
    33  
    34  func TestGetCallFlags(t *testing.T) {
    35  	bc, _ := chain.NewSingle(t)
    36  	ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
    37  	require.NoError(t, err)
    38  
    39  	ic.VM.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All)
    40  	require.NoError(t, contract.GetCallFlags(ic))
    41  	require.Equal(t, int64(callflag.All), ic.VM.Estack().Pop().Value().(*big.Int).Int64())
    42  }
    43  
    44  func TestCall(t *testing.T) {
    45  	bc, _ := chain.NewSingle(t)
    46  	ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
    47  	require.NoError(t, err)
    48  
    49  	cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test
    50  	require.NoError(t, native.PutContractState(ic.DAO, cs))
    51  	require.NoError(t, native.PutContractState(ic.DAO, currCs))
    52  
    53  	currScript := currCs.NEF.Script
    54  	h := cs.Hash
    55  
    56  	addArgs := stackitem.NewArray([]stackitem.Item{stackitem.Make(1), stackitem.Make(2)})
    57  	t.Run("Good", func(t *testing.T) {
    58  		t.Run("2 arguments", func(t *testing.T) {
    59  			loadScript(ic, currScript, 42)
    60  			ic.VM.Estack().PushVal(addArgs)
    61  			ic.VM.Estack().PushVal(callflag.All)
    62  			ic.VM.Estack().PushVal("add")
    63  			ic.VM.Estack().PushVal(h.BytesBE())
    64  			require.NoError(t, contract.Call(ic))
    65  			require.NoError(t, ic.VM.Run())
    66  			require.Equal(t, 2, ic.VM.Estack().Len())
    67  			require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
    68  			require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
    69  		})
    70  		t.Run("3 arguments", func(t *testing.T) {
    71  			loadScript(ic, currScript, 42)
    72  			ic.VM.Estack().PushVal(stackitem.NewArray(
    73  				append(addArgs.Value().([]stackitem.Item), stackitem.Make(3))))
    74  			ic.VM.Estack().PushVal(callflag.All)
    75  			ic.VM.Estack().PushVal("add")
    76  			ic.VM.Estack().PushVal(h.BytesBE())
    77  			require.NoError(t, contract.Call(ic))
    78  			require.NoError(t, ic.VM.Run())
    79  			require.Equal(t, 2, ic.VM.Estack().Len())
    80  			require.Equal(t, big.NewInt(6), ic.VM.Estack().Pop().Value())
    81  			require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
    82  		})
    83  	})
    84  
    85  	t.Run("CallExInvalidFlag", func(t *testing.T) {
    86  		loadScript(ic, currScript, 42)
    87  		ic.VM.Estack().PushVal(addArgs)
    88  		ic.VM.Estack().PushVal(byte(0xFF))
    89  		ic.VM.Estack().PushVal("add")
    90  		ic.VM.Estack().PushVal(h.BytesBE())
    91  		require.Error(t, contract.Call(ic))
    92  	})
    93  
    94  	runInvalid := func(args ...any) func(t *testing.T) {
    95  		return func(t *testing.T) {
    96  			loadScriptWithHashAndFlags(ic, currScript, h, callflag.All, 42)
    97  			for i := range args {
    98  				ic.VM.Estack().PushVal(args[i])
    99  			}
   100  			// interops can both return error and panic,
   101  			// we don't care which kind of error has occurred
   102  			require.Panics(t, func() {
   103  				err := contract.Call(ic)
   104  				if err != nil {
   105  					panic(err)
   106  				}
   107  			})
   108  		}
   109  	}
   110  
   111  	t.Run("Invalid", func(t *testing.T) {
   112  		t.Run("Hash", runInvalid(addArgs, "add", h.BytesBE()[1:]))
   113  		t.Run("MissingHash", runInvalid(addArgs, "add", util.Uint160{}.BytesBE()))
   114  		t.Run("Method", runInvalid(addArgs, stackitem.NewInterop("add"), h.BytesBE()))
   115  		t.Run("MissingMethod", runInvalid(addArgs, "sub", h.BytesBE()))
   116  		t.Run("DisallowedMethod", runInvalid(stackitem.NewArray(nil), "ret7", h.BytesBE()))
   117  		t.Run("Arguments", runInvalid(1, "add", h.BytesBE()))
   118  		t.Run("NotEnoughArguments", runInvalid(
   119  			stackitem.NewArray([]stackitem.Item{stackitem.Make(1)}), "add", h.BytesBE()))
   120  		t.Run("TooMuchArguments", runInvalid(
   121  			stackitem.NewArray([]stackitem.Item{
   122  				stackitem.Make(1), stackitem.Make(2), stackitem.Make(3), stackitem.Make(4)}),
   123  			"add", h.BytesBE()))
   124  	})
   125  
   126  	t.Run("ReturnValues", func(t *testing.T) {
   127  		t.Run("Many", func(t *testing.T) {
   128  			loadScript(ic, currScript, 42)
   129  			ic.VM.Estack().PushVal(stackitem.NewArray(nil))
   130  			ic.VM.Estack().PushVal(callflag.All)
   131  			ic.VM.Estack().PushVal("invalidReturn")
   132  			ic.VM.Estack().PushVal(h.BytesBE())
   133  			require.NoError(t, contract.Call(ic))
   134  			require.Error(t, ic.VM.Run())
   135  		})
   136  		t.Run("Void", func(t *testing.T) {
   137  			loadScript(ic, currScript, 42)
   138  			ic.VM.Estack().PushVal(stackitem.NewArray(nil))
   139  			ic.VM.Estack().PushVal(callflag.All)
   140  			ic.VM.Estack().PushVal("justReturn")
   141  			ic.VM.Estack().PushVal(h.BytesBE())
   142  			require.NoError(t, contract.Call(ic))
   143  			require.NoError(t, ic.VM.Run())
   144  			require.Equal(t, 2, ic.VM.Estack().Len())
   145  			require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
   146  			require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
   147  		})
   148  	})
   149  
   150  	t.Run("IsolatedStack", func(t *testing.T) {
   151  		loadScript(ic, currScript, 42)
   152  		ic.VM.Estack().PushVal(stackitem.NewArray(nil))
   153  		ic.VM.Estack().PushVal(callflag.All)
   154  		ic.VM.Estack().PushVal("drop")
   155  		ic.VM.Estack().PushVal(h.BytesBE())
   156  		require.NoError(t, contract.Call(ic))
   157  		require.Error(t, ic.VM.Run())
   158  	})
   159  
   160  	t.Run("CallInitialize", func(t *testing.T) {
   161  		t.Run("Directly", runInvalid(stackitem.NewArray([]stackitem.Item{}), "_initialize", h.BytesBE()))
   162  
   163  		loadScript(ic, currScript, 42)
   164  		ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
   165  		ic.VM.Estack().PushVal(callflag.All)
   166  		ic.VM.Estack().PushVal("add3")
   167  		ic.VM.Estack().PushVal(h.BytesBE())
   168  		require.NoError(t, contract.Call(ic))
   169  		require.NoError(t, ic.VM.Run())
   170  		require.Equal(t, 2, ic.VM.Estack().Len())
   171  		require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
   172  		require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
   173  	})
   174  }
   175  
   176  func TestLoadToken(t *testing.T) {
   177  	bc, acc := chain.NewSingle(t)
   178  	e := neotest.NewExecutor(t, bc, acc, acc)
   179  	managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management))
   180  
   181  	cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash())
   182  	rawManifest, err := json.Marshal(cs.Manifest)
   183  	require.NoError(t, err)
   184  	rawNef, err := cs.NEF.Bytes()
   185  	require.NoError(t, err)
   186  	tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest)
   187  	e.AddNewBlock(t, tx)
   188  	e.CheckHalt(t, tx.Hash())
   189  	cInvoker := e.ValidatorInvoker(cs.Hash)
   190  
   191  	t.Run("good", func(t *testing.T) {
   192  		realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash())
   193  		cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash())
   194  	})
   195  	t.Run("invalid param count", func(t *testing.T) {
   196  		cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash())
   197  	})
   198  	t.Run("invalid contract", func(t *testing.T) {
   199  		cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1")
   200  	})
   201  }
   202  
   203  func TestSnapshotIsolation_Exceptions(t *testing.T) {
   204  	bc, acc := chain.NewSingle(t)
   205  	e := neotest.NewExecutor(t, bc, acc, acc)
   206  
   207  	// Contract A puts value in the storage, emits notifications and panics.
   208  	srcA := `package contractA
   209  		import (
   210  			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
   211  			"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
   212  			"github.com/nspcc-dev/neo-go/pkg/interop/storage"
   213  		)
   214  		func DoAndPanic(key, value []byte, nNtf int) int { // avoid https://github.com/nspcc-dev/neo-go/issues/2509
   215  			c := storage.GetContext()
   216  			storage.Put(c, key, value)
   217  			for i := 0; i < nNtf; i++ {
   218  				runtime.Notify("NotificationFromA", i)
   219  			}
   220  			panic("panic from A")
   221  		}
   222  		func CheckA(key []byte, nNtf int) bool {
   223  			c := storage.GetContext()
   224  			value := storage.Get(c, key)
   225  			// If called from B, then no storage changes made by A should be visible by this moment (they have been discarded after exception handling).
   226  			if value != nil {
   227  				return false
   228  			}
   229  			notifications := runtime.GetNotifications(nil)
   230  			if len(notifications) != nNtf {
   231  				return false
   232  			}
   233  			// If called from B, then no notifications made by A should be visible by this moment (they have been discarded after exception handling).
   234  			for i := 0; i < len(notifications); i++ {
   235  				ntf := notifications[i]
   236  				name := string(ntf[1].([]byte))
   237  				if name == "NotificationFromA" {
   238  					return false
   239  				}
   240  			}
   241  			return true
   242  		}
   243  		func CheckB() bool {
   244  			return contract.Call(runtime.GetCallingScriptHash(), "checkStorageChanges", contract.All).(bool)
   245  		}`
   246  	ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
   247  		NoEventsCheck:      true,
   248  		NoPermissionsCheck: true,
   249  		Name:               "contractA",
   250  		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
   251  		ContractEvents: []compiler.HybridEvent{
   252  			{Name: "NotificationFromA", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
   253  		},
   254  	})
   255  	e.DeployContract(t, ctrA, nil)
   256  
   257  	var hashAStr string
   258  	for i := 0; i < util.Uint160Size; i++ {
   259  		hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
   260  		if i != util.Uint160Size-1 {
   261  			hashAStr += ", "
   262  		}
   263  	}
   264  	// Contract B puts value in the storage, emits notifications and calls A either
   265  	// in try-catch block or without it. After that checks that proper notifications
   266  	// and storage changes are available from different contexts.
   267  	srcB := `package contractB
   268  		import (
   269  			"github.com/nspcc-dev/neo-go/pkg/interop"
   270  			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
   271  			"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
   272  			"github.com/nspcc-dev/neo-go/pkg/interop/storage"
   273  			"github.com/nspcc-dev/neo-go/pkg/interop/util"
   274  		)
   275  		var caughtKey = []byte("caught")
   276  		func DoAndCatch(shouldRecover bool, keyA, valueA, keyB, valueB []byte, nNtfA, nNtfB1, nNtfB2 int) {
   277  			if shouldRecover {
   278  				defer func() {
   279  					if r := recover(); r != nil {
   280  						keyA := []byte("keyA") // defer can not capture variables from outside
   281  						nNtfB1 := 2
   282  						nNtfB2 := 4
   283  						c := storage.GetContext()
   284  						storage.Put(c, caughtKey, []byte{})
   285  						for i := 0; i < nNtfB2; i++ {
   286  							runtime.Notify("NotificationFromB after panic", i)
   287  						}
   288  						// Check that storage changes and notifications made by A are reverted.
   289  						ok := contract.Call(interop.Hash160{` + hashAStr + `}, "checkA", contract.All, keyA, nNtfB1+nNtfB2).(bool)
   290  						if !ok {
   291  							util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
   292  						}
   293  						// Check that storage changes made by B after catch are still available in current context.
   294  						ok = CheckStorageChanges()
   295  						if !ok {
   296  							util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
   297  						}
   298  						// Check that storage changes made by B after catch are still available from the outside context.
   299  						ok = contract.Call(interop.Hash160{` + hashAStr + `}, "checkB", contract.All).(bool)
   300  						if !ok {
   301  							util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
   302  						}
   303  					}
   304  				}()
   305  			}
   306  			c := storage.GetContext()
   307  			storage.Put(c, keyB, valueB)
   308  			for i := 0; i < nNtfB1; i++ {
   309  				runtime.Notify("NotificationFromB before panic", i)
   310  			}
   311  			internalCaller(keyA, valueA, nNtfA)
   312  		}
   313  		func internalCaller(keyA, valueA []byte, nNtfA int) {
   314  			contract.Call(interop.Hash160{` + hashAStr + `}, "doAndPanic", contract.All, keyA, valueA, nNtfA)
   315  		}
   316  		func CheckStorageChanges() bool {
   317  			c := storage.GetContext()
   318  			itm := storage.Get(c, caughtKey)
   319  			return itm != nil
   320  		}`
   321  	ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
   322  		Name:               "contractB",
   323  		NoEventsCheck:      true,
   324  		NoPermissionsCheck: true,
   325  		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
   326  		ContractEvents: []compiler.HybridEvent{
   327  			{Name: "NotificationFromB before panic", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
   328  			{Name: "NotificationFromB after panic", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
   329  		},
   330  	})
   331  	e.DeployContract(t, ctrB, nil)
   332  
   333  	keyA := []byte("keyA")     // hard-coded in the contract code due to `defer` inability to capture variables from outside.
   334  	valueA := []byte("valueA") // hard-coded in the contract code
   335  	keyB := []byte("keyB")
   336  	valueB := []byte("valueB")
   337  	nNtfA := 3
   338  	nNtfBBeforePanic := 2 // hard-coded in the contract code
   339  	nNtfBAfterPanic := 4  // hard-coded in the contract code
   340  	ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
   341  
   342  	// Firstly, do not catch exception and check that all notifications are presented in the notifications list.
   343  	h := ctrInvoker.InvokeFail(t, `unhandled exception: "panic from A"`, "doAndCatch", false, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic)
   344  	aer := e.GetTxExecResult(t, h)
   345  	require.Equal(t, nNtfBBeforePanic+nNtfA, len(aer.Events))
   346  
   347  	// Then catch exception thrown by A and check that only notifications/storage changes from B are saved.
   348  	h = ctrInvoker.Invoke(t, stackitem.Null{}, "doAndCatch", true, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic)
   349  	aer = e.GetTxExecResult(t, h)
   350  	require.Equal(t, nNtfBBeforePanic+nNtfBAfterPanic, len(aer.Events))
   351  }
   352  
   353  // This test is written to test nested calls with try-catch block and proper notifications handling.
   354  func TestSnapshotIsolation_NestedContextException(t *testing.T) {
   355  	bc, acc := chain.NewSingle(t)
   356  	e := neotest.NewExecutor(t, bc, acc, acc)
   357  
   358  	srcA := `package contractA
   359  		import (
   360  			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
   361  			"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
   362  		)
   363  		func CallA() {
   364  			runtime.Notify("Calling A")
   365  			contract.Call(runtime.GetExecutingScriptHash(), "a", contract.All)
   366  			runtime.Notify("Finish")
   367  		}
   368  		func A() {
   369  			defer func() {
   370  				if r := recover(); r != nil {
   371  					runtime.Notify("Caught")
   372  				}
   373  			}()
   374  			runtime.Notify("A")
   375  			contract.Call(runtime.GetExecutingScriptHash(), "b", contract.All)
   376  			runtime.Notify("Unreachable A")
   377  		}
   378  		func B() int {
   379  			runtime.Notify("B")
   380  			contract.Call(runtime.GetExecutingScriptHash(), "c", contract.All)
   381  			runtime.Notify("Unreachable B")
   382  			return 5
   383  		}
   384  		func C() {
   385  			runtime.Notify("C")
   386  			panic("exception from C")
   387  		}`
   388  	ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
   389  		NoEventsCheck:      true,
   390  		NoPermissionsCheck: true,
   391  		Name:               "contractA",
   392  		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
   393  		ContractEvents: []compiler.HybridEvent{
   394  			{Name: "Calling A"},
   395  			{Name: "Finish"},
   396  			{Name: "Caught"},
   397  			{Name: "A"},
   398  			{Name: "Unreachable A"},
   399  			{Name: "B"},
   400  			{Name: "Unreachable B"},
   401  			{Name: "C"},
   402  		},
   403  	})
   404  	e.DeployContract(t, ctrA, nil)
   405  
   406  	ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
   407  	h := ctrInvoker.Invoke(t, stackitem.Null{}, "callA")
   408  	aer := e.GetTxExecResult(t, h)
   409  	require.Equal(t, 4, len(aer.Events))
   410  	require.Equal(t, "Calling A", aer.Events[0].Name)
   411  	require.Equal(t, "A", aer.Events[1].Name)
   412  	require.Equal(t, "Caught", aer.Events[2].Name)
   413  	require.Equal(t, "Finish", aer.Events[3].Name)
   414  }
   415  
   416  // This test is written to avoid https://github.com/neo-project/neo/issues/2746.
   417  func TestSnapshotIsolation_CallToItself(t *testing.T) {
   418  	bc, acc := chain.NewSingle(t)
   419  	e := neotest.NewExecutor(t, bc, acc, acc)
   420  
   421  	// Contract A calls method of self and throws if storage changes made by Do are unavailable after call to it.
   422  	srcA := `package contractA
   423  		import (
   424  			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
   425  			"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
   426  			"github.com/nspcc-dev/neo-go/pkg/interop/storage"
   427  		)
   428  		var key = []byte("key")
   429  		func Test() {
   430  			contract.Call(runtime.GetExecutingScriptHash(), "callMyselfAndCheck", contract.All)
   431  		}
   432  		func CallMyselfAndCheck() {
   433  			contract.Call(runtime.GetExecutingScriptHash(), "do", contract.All)
   434  			c := storage.GetContext()
   435  			val := storage.Get(c, key)
   436  			if val == nil {
   437  				panic("changes from previous context were not persisted")
   438  			}
   439  		}
   440  		func Do() {
   441  			c := storage.GetContext()
   442  			storage.Put(c, key, []byte("value"))
   443  		}
   444  		func Check() {
   445  			c := storage.GetContext()
   446  			val := storage.Get(c, key)
   447  			if val == nil {
   448  				panic("value is nil")
   449  			}
   450  		}
   451  `
   452  	ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
   453  		NoEventsCheck:      true,
   454  		NoPermissionsCheck: true,
   455  		Name:               "contractA",
   456  		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
   457  	})
   458  	e.DeployContract(t, ctrA, nil)
   459  
   460  	ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
   461  	ctrInvoker.Invoke(t, stackitem.Null{}, "test")
   462  
   463  	// A separate call is needed to check whether all VM contexts were properly
   464  	// unwrapped and persisted during the previous call.
   465  	ctrInvoker.Invoke(t, stackitem.Null{}, "check")
   466  }
   467  
   468  // This test is written to check https://github.com/nspcc-dev/neo-go/issues/2509
   469  // and https://github.com/neo-project/neo/pull/2745#discussion_r879167180.
   470  func TestRET_after_FINALLY_PanicInsideVoidMethod(t *testing.T) {
   471  	bc, acc := chain.NewSingle(t)
   472  	e := neotest.NewExecutor(t, bc, acc, acc)
   473  
   474  	// Contract A throws catchable exception. It also has a non-void method.
   475  	srcA := `package contractA
   476  		func Panic() {
   477  			panic("panic from A")
   478  		}
   479  		func ReturnSomeValue() int {
   480  			return 5
   481  		}`
   482  	ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
   483  		NoEventsCheck:      true,
   484  		NoPermissionsCheck: true,
   485  		Name:               "contractA",
   486  	})
   487  	e.DeployContract(t, ctrA, nil)
   488  
   489  	var hashAStr string
   490  	for i := 0; i < util.Uint160Size; i++ {
   491  		hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
   492  		if i != util.Uint160Size-1 {
   493  			hashAStr += ", "
   494  		}
   495  	}
   496  	// Contract B calls A and catches the exception thrown by A.
   497  	srcB := `package contractB
   498  		import (
   499  			"github.com/nspcc-dev/neo-go/pkg/interop"
   500  			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
   501  		)
   502  		func Catch() {
   503  			defer func() {
   504  				if r := recover(); r != nil {
   505  					// Call method with return value to check https://github.com/neo-project/neo/pull/2745#discussion_r879167180.
   506  					contract.Call(interop.Hash160{` + hashAStr + `}, "returnSomeValue", contract.All)
   507  				}
   508  			}()
   509  			contract.Call(interop.Hash160{` + hashAStr + `}, "panic", contract.All)
   510  		}`
   511  	ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
   512  		Name:               "contractB",
   513  		NoEventsCheck:      true,
   514  		NoPermissionsCheck: true,
   515  		Permissions: []manifest.Permission{
   516  			{
   517  				Methods: manifest.WildStrings{Value: nil},
   518  			},
   519  		},
   520  	})
   521  	e.DeployContract(t, ctrB, nil)
   522  
   523  	ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
   524  	ctrInvoker.Invoke(t, stackitem.Null{}, "catch")
   525  }
   526  
   527  // This test is written to check https://github.com/neo-project/neo/pull/2745#discussion_r879125733.
   528  func TestRET_after_FINALLY_CallNonVoidAfterVoidMethod(t *testing.T) {
   529  	bc, acc := chain.NewSingle(t)
   530  	e := neotest.NewExecutor(t, bc, acc, acc)
   531  
   532  	// Contract A has two methods. One of them has no return value, and the other has it.
   533  	srcA := `package contractA
   534  		import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
   535  		func NoRet() {
   536  			runtime.Log("no ret")
   537  		}
   538  		func HasRet() int {
   539  			runtime.Log("ret")
   540  			return 5
   541  		}`
   542  	ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
   543  		NoEventsCheck:      true,
   544  		NoPermissionsCheck: true,
   545  		Name:               "contractA",
   546  	})
   547  	e.DeployContract(t, ctrA, nil)
   548  
   549  	var hashAStr string
   550  	for i := 0; i < util.Uint160Size; i++ {
   551  		hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
   552  		if i != util.Uint160Size-1 {
   553  			hashAStr += ", "
   554  		}
   555  	}
   556  	// Contract B calls A in try-catch block.
   557  	srcB := `package contractB
   558  		import (
   559  			"github.com/nspcc-dev/neo-go/pkg/interop"
   560  			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
   561  			"github.com/nspcc-dev/neo-go/pkg/interop/util"
   562  		)
   563  		func CallAInTryCatch() {
   564  			defer func() {
   565  				if r := recover(); r != nil {
   566  					util.Abort() // should never happen
   567  				}
   568  			}()
   569  			contract.Call(interop.Hash160{` + hashAStr + `}, "noRet", contract.All)
   570  			contract.Call(interop.Hash160{` + hashAStr + `}, "hasRet", contract.All)
   571  		}`
   572  	ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
   573  		Name:               "contractB",
   574  		NoEventsCheck:      true,
   575  		NoPermissionsCheck: true,
   576  		Permissions: []manifest.Permission{
   577  			{
   578  				Methods: manifest.WildStrings{Value: nil},
   579  			},
   580  		},
   581  	})
   582  	e.DeployContract(t, ctrB, nil)
   583  
   584  	ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
   585  	h := ctrInvoker.Invoke(t, stackitem.Null{}, "callAInTryCatch")
   586  	aer := e.GetTxExecResult(t, h)
   587  
   588  	require.Equal(t, 1, len(aer.Stack))
   589  }
   590  
   591  // This test is created to check https://github.com/neo-project/neo/pull/2755#discussion_r880087983.
   592  func TestCALLL_from_VoidContext(t *testing.T) {
   593  	bc, acc := chain.NewSingle(t)
   594  	e := neotest.NewExecutor(t, bc, acc, acc)
   595  
   596  	// Contract A has void method `CallHasRet` which calls non-void method `HasRet`.
   597  	srcA := `package contractA
   598  		func CallHasRet() { // Creates a context with non-nil onUnload.
   599  			HasRet()
   600  		}
   601  		func HasRet() int { // CALL_L clones parent context, check that onUnload is not cloned.
   602  			return 5
   603  		}`
   604  	ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
   605  		NoEventsCheck:      true,
   606  		NoPermissionsCheck: true,
   607  		Name:               "contractA",
   608  	})
   609  	e.DeployContract(t, ctrA, nil)
   610  
   611  	ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
   612  	ctrInvoker.Invoke(t, stackitem.Null{}, "callHasRet")
   613  }
   614  
   615  func loadScript(ic *interop.Context, script []byte, args ...any) {
   616  	ic.SpawnVM()
   617  	ic.VM.LoadScriptWithFlags(script, callflag.AllowCall)
   618  	for i := range args {
   619  		ic.VM.Estack().PushVal(args[i])
   620  	}
   621  	ic.VM.GasLimit = -1
   622  }
   623  
   624  func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...any) {
   625  	ic.SpawnVM()
   626  	ic.VM.LoadScriptWithHash(script, hash, f)
   627  	for i := range args {
   628  		ic.VM.Estack().PushVal(args[i])
   629  	}
   630  	ic.VM.GasLimit = -1
   631  }