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 }