neo-go/pkg/core/interop/contract/call_test.go
Anna Shaleva a7aceca74a interop: use currently executing contract state for permissions check
It's not correct to use an updated contract state got from Management to
check for the allowed method call. We need to use manifest from the
currently executing context for that. It may be critical for cases when
executing contract is being updated firstly, and after that calls
another contract. So we need an old (executing) contract manifest for
this check.

This change likely does not affect the mainnet's state since it's hard
to meet the trigger criteria, but I'd put it under the hardfork anyway.

Ref. https://github.com/neo-project/neo/pull/3290.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2024-06-11 19:13:17 +03:00

743 lines
26 KiB
Go

package contract_test
import (
"encoding/json"
"fmt"
"math/big"
"path/filepath"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts")
func TestGetCallFlags(t *testing.T) {
bc, _ := chain.NewSingle(t)
ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
ic.VM.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All)
require.NoError(t, contract.GetCallFlags(ic))
require.Equal(t, int64(callflag.All), ic.VM.Estack().Pop().Value().(*big.Int).Int64())
}
func TestCall(t *testing.T) {
bc, _ := chain.NewSingle(t)
ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test
require.NoError(t, native.PutContractState(ic.DAO, cs))
require.NoError(t, native.PutContractState(ic.DAO, currCs))
currScript := currCs.NEF.Script
h := cs.Hash
addArgs := stackitem.NewArray([]stackitem.Item{stackitem.Make(1), stackitem.Make(2)})
t.Run("Good", func(t *testing.T) {
t.Run("2 arguments", func(t *testing.T) {
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(addArgs)
ic.VM.Estack().PushVal(callflag.All)
ic.VM.Estack().PushVal("add")
ic.VM.Estack().PushVal(h.BytesBE())
require.NoError(t, contract.Call(ic))
require.NoError(t, ic.VM.Run())
require.Equal(t, 2, ic.VM.Estack().Len())
require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
})
t.Run("3 arguments", func(t *testing.T) {
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(stackitem.NewArray(
append(addArgs.Value().([]stackitem.Item), stackitem.Make(3))))
ic.VM.Estack().PushVal(callflag.All)
ic.VM.Estack().PushVal("add")
ic.VM.Estack().PushVal(h.BytesBE())
require.NoError(t, contract.Call(ic))
require.NoError(t, ic.VM.Run())
require.Equal(t, 2, ic.VM.Estack().Len())
require.Equal(t, big.NewInt(6), ic.VM.Estack().Pop().Value())
require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
})
})
t.Run("CallExInvalidFlag", func(t *testing.T) {
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(addArgs)
ic.VM.Estack().PushVal(byte(0xFF))
ic.VM.Estack().PushVal("add")
ic.VM.Estack().PushVal(h.BytesBE())
require.Error(t, contract.Call(ic))
})
runInvalid := func(args ...any) func(t *testing.T) {
return func(t *testing.T) {
loadScriptWithHashAndFlags(ic, currScript, h, callflag.All, 42)
for i := range args {
ic.VM.Estack().PushVal(args[i])
}
// interops can both return error and panic,
// we don't care which kind of error has occurred
require.Panics(t, func() {
err := contract.Call(ic)
if err != nil {
panic(err)
}
})
}
}
t.Run("Invalid", func(t *testing.T) {
t.Run("Hash", runInvalid(addArgs, "add", h.BytesBE()[1:]))
t.Run("MissingHash", runInvalid(addArgs, "add", util.Uint160{}.BytesBE()))
t.Run("Method", runInvalid(addArgs, stackitem.NewInterop("add"), h.BytesBE()))
t.Run("MissingMethod", runInvalid(addArgs, "sub", h.BytesBE()))
t.Run("DisallowedMethod", runInvalid(stackitem.NewArray(nil), "ret7", h.BytesBE()))
t.Run("Arguments", runInvalid(1, "add", h.BytesBE()))
t.Run("NotEnoughArguments", runInvalid(
stackitem.NewArray([]stackitem.Item{stackitem.Make(1)}), "add", h.BytesBE()))
t.Run("TooMuchArguments", runInvalid(
stackitem.NewArray([]stackitem.Item{
stackitem.Make(1), stackitem.Make(2), stackitem.Make(3), stackitem.Make(4)}),
"add", h.BytesBE()))
})
t.Run("ReturnValues", func(t *testing.T) {
t.Run("Many", func(t *testing.T) {
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
ic.VM.Estack().PushVal(callflag.All)
ic.VM.Estack().PushVal("invalidReturn")
ic.VM.Estack().PushVal(h.BytesBE())
require.NoError(t, contract.Call(ic))
require.Error(t, ic.VM.Run())
})
t.Run("Void", func(t *testing.T) {
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
ic.VM.Estack().PushVal(callflag.All)
ic.VM.Estack().PushVal("justReturn")
ic.VM.Estack().PushVal(h.BytesBE())
require.NoError(t, contract.Call(ic))
require.NoError(t, ic.VM.Run())
require.Equal(t, 2, ic.VM.Estack().Len())
require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
})
})
t.Run("IsolatedStack", func(t *testing.T) {
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
ic.VM.Estack().PushVal(callflag.All)
ic.VM.Estack().PushVal("drop")
ic.VM.Estack().PushVal(h.BytesBE())
require.NoError(t, contract.Call(ic))
require.Error(t, ic.VM.Run())
})
t.Run("CallInitialize", func(t *testing.T) {
t.Run("Directly", runInvalid(stackitem.NewArray([]stackitem.Item{}), "_initialize", h.BytesBE()))
loadScript(ic, currScript, 42)
ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
ic.VM.Estack().PushVal(callflag.All)
ic.VM.Estack().PushVal("add3")
ic.VM.Estack().PushVal(h.BytesBE())
require.NoError(t, contract.Call(ic))
require.NoError(t, ic.VM.Run())
require.Equal(t, 2, ic.VM.Estack().Len())
require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
})
}
func TestSystemContractCall_Permissions(t *testing.T) {
check := func(t *testing.T, cfg func(*config.Blockchain), shouldUpdateFail bool) {
bc, acc := chain.NewSingleWithCustomConfig(t, cfg)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A has an unsafe method.
srcA := `package contractA
func RetOne() int {
return 1
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
})
e.DeployContract(t, ctrA, nil)
var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}
// Contract B has a method that calls contract's A and another update method that
// calls contract's A after B's update.
srcB := `package contractB
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
)
func CallRetOne() int {
res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
return res
}
func Update(nef []byte, manifest []byte) int {
management.Update(nef, manifest)
res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
return res
}`
ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
Methods: manifest.WildStrings{Value: []string{"retOne"}},
},
{
Methods: manifest.WildStrings{Value: []string{"update"}},
},
},
})
e.DeployContract(t, ctrB, nil)
ctrBInvoker := e.ValidatorInvoker(ctrB.Hash)
// ctrBUpdated differs from ctrB in that it has no permission to call retOne method of ctrA
ctrBUpdated := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
Methods: manifest.WildStrings{Value: []string{}},
},
{
Methods: manifest.WildStrings{Value: []string{"update"}},
},
},
})
// Call to A before B update should HALT.
ctrBInvoker.Invoke(t, stackitem.Make(1), "callRetOne")
// Call to A in the same context as B update should HALT.
n, err := ctrBUpdated.NEF.Bytes()
require.NoError(t, err)
m, err := json.Marshal(ctrBUpdated.Manifest)
require.NoError(t, err)
if shouldUpdateFail {
ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "update", n, m)
} else {
ctrBInvoker.Invoke(t, stackitem.Make(1), "update", n, m)
}
// If contract is updated, then all to A after B update should FAULT (manifest
// is updated, no permission to call retOne method).
if !shouldUpdateFail {
ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "callRetOne")
}
}
// Pre-Domovoi behaviour: an updated contract state is used for permissions check.
check(t, func(cfg *config.Blockchain) {
cfg.Hardforks = map[string]uint32{
config.HFDomovoi.String(): 100500,
}
}, true)
// Post-Domovoi behaviour: an executing contract state is used for permissions check.
check(t, func(cfg *config.Blockchain) {
cfg.Hardforks = map[string]uint32{
config.HFDomovoi.String(): 0,
}
}, false)
}
func TestLoadToken(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management))
cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash())
rawManifest, err := json.Marshal(cs.Manifest)
require.NoError(t, err)
rawNef, err := cs.NEF.Bytes()
require.NoError(t, err)
tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest)
e.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash())
cInvoker := e.ValidatorInvoker(cs.Hash)
t.Run("good", func(t *testing.T) {
realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash())
cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash())
})
t.Run("invalid param count", func(t *testing.T) {
cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash())
})
t.Run("invalid contract", func(t *testing.T) {
cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1")
})
}
func TestSnapshotIsolation_Exceptions(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A puts value in the storage, emits notifications and panics.
srcA := `package contractA
import (
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
func DoAndPanic(key, value []byte, nNtf int) int { // avoid https://github.com/nspcc-dev/neo-go/issues/2509
c := storage.GetContext()
storage.Put(c, key, value)
for i := 0; i < nNtf; i++ {
runtime.Notify("NotificationFromA", i)
}
panic("panic from A")
}
func CheckA(key []byte, nNtf int) bool {
c := storage.GetContext()
value := storage.Get(c, key)
// If called from B, then no storage changes made by A should be visible by this moment (they have been discarded after exception handling).
if value != nil {
return false
}
notifications := runtime.GetNotifications(nil)
if len(notifications) != nNtf {
return false
}
// If called from B, then no notifications made by A should be visible by this moment (they have been discarded after exception handling).
for i := 0; i < len(notifications); i++ {
ntf := notifications[i]
name := string(ntf[1].([]byte))
if name == "NotificationFromA" {
return false
}
}
return true
}
func CheckB() bool {
return contract.Call(runtime.GetCallingScriptHash(), "checkStorageChanges", contract.All).(bool)
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
ContractEvents: []compiler.HybridEvent{
{Name: "NotificationFromA", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
},
})
e.DeployContract(t, ctrA, nil)
var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}
// Contract B puts value in the storage, emits notifications and calls A either
// in try-catch block or without it. After that checks that proper notifications
// and storage changes are available from different contexts.
srcB := `package contractB
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
var caughtKey = []byte("caught")
func DoAndCatch(shouldRecover bool, keyA, valueA, keyB, valueB []byte, nNtfA, nNtfB1, nNtfB2 int) {
if shouldRecover {
defer func() {
if r := recover(); r != nil {
keyA := []byte("keyA") // defer can not capture variables from outside
nNtfB1 := 2
nNtfB2 := 4
c := storage.GetContext()
storage.Put(c, caughtKey, []byte{})
for i := 0; i < nNtfB2; i++ {
runtime.Notify("NotificationFromB after panic", i)
}
// Check that storage changes and notifications made by A are reverted.
ok := contract.Call(interop.Hash160{` + hashAStr + `}, "checkA", contract.All, keyA, nNtfB1+nNtfB2).(bool)
if !ok {
util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
}
// Check that storage changes made by B after catch are still available in current context.
ok = CheckStorageChanges()
if !ok {
util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
}
// Check that storage changes made by B after catch are still available from the outside context.
ok = contract.Call(interop.Hash160{` + hashAStr + `}, "checkB", contract.All).(bool)
if !ok {
util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
}
}
}()
}
c := storage.GetContext()
storage.Put(c, keyB, valueB)
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 {
c := storage.GetContext()
itm := storage.Get(c, caughtKey)
return itm != nil
}`
ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
ContractEvents: []compiler.HybridEvent{
{Name: "NotificationFromB before panic", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
{Name: "NotificationFromB after panic", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
},
})
e.DeployContract(t, ctrB, nil)
keyA := []byte("keyA") // hard-coded in the contract code due to `defer` inability to capture variables from outside.
valueA := []byte("valueA") // hard-coded in the contract code
keyB := []byte("keyB")
valueB := []byte("valueB")
nNtfA := 3
nNtfBBeforePanic := 2 // hard-coded in the contract code
nNtfBAfterPanic := 4 // hard-coded in the contract code
ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
// Firstly, do not catch exception and check that all notifications are presented in the notifications list.
h := ctrInvoker.InvokeFail(t, `unhandled exception: "panic from A"`, "doAndCatch", false, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic)
aer := e.GetTxExecResult(t, h)
require.Equal(t, nNtfBBeforePanic+nNtfA, len(aer.Events))
// Then catch exception thrown by A and check that only notifications/storage changes from B are saved.
h = ctrInvoker.Invoke(t, stackitem.Null{}, "doAndCatch", true, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic)
aer = e.GetTxExecResult(t, h)
require.Equal(t, nNtfBBeforePanic+nNtfBAfterPanic, len(aer.Events))
}
// This test is written to test nested calls with try-catch block and proper notifications handling.
func TestSnapshotIsolation_NestedContextException(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
srcA := `package contractA
import (
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
func CallA() {
runtime.Notify("Calling A")
contract.Call(runtime.GetExecutingScriptHash(), "a", contract.All)
runtime.Notify("Finish")
}
func A() {
defer func() {
if r := recover(); r != nil {
runtime.Notify("Caught")
}
}()
runtime.Notify("A")
contract.Call(runtime.GetExecutingScriptHash(), "b", contract.All)
runtime.Notify("Unreachable A")
}
func B() int {
runtime.Notify("B")
contract.Call(runtime.GetExecutingScriptHash(), "c", contract.All)
runtime.Notify("Unreachable B")
return 5
}
func C() {
runtime.Notify("C")
panic("exception from C")
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
ContractEvents: []compiler.HybridEvent{
{Name: "Calling A"},
{Name: "Finish"},
{Name: "Caught"},
{Name: "A"},
{Name: "Unreachable A"},
{Name: "B"},
{Name: "Unreachable B"},
{Name: "C"},
},
})
e.DeployContract(t, ctrA, nil)
ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
h := ctrInvoker.Invoke(t, stackitem.Null{}, "callA")
aer := e.GetTxExecResult(t, h)
require.Equal(t, 4, len(aer.Events))
require.Equal(t, "Calling A", aer.Events[0].Name)
require.Equal(t, "A", aer.Events[1].Name)
require.Equal(t, "Caught", aer.Events[2].Name)
require.Equal(t, "Finish", aer.Events[3].Name)
}
// This test is written to avoid https://github.com/neo-project/neo/issues/2746.
func TestSnapshotIsolation_CallToItself(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A calls method of self and throws if storage changes made by Do are unavailable after call to it.
srcA := `package contractA
import (
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
var key = []byte("key")
func Test() {
contract.Call(runtime.GetExecutingScriptHash(), "callMyselfAndCheck", contract.All)
}
func CallMyselfAndCheck() {
contract.Call(runtime.GetExecutingScriptHash(), "do", contract.All)
c := storage.GetContext()
val := storage.Get(c, key)
if val == nil {
panic("changes from previous context were not persisted")
}
}
func Do() {
c := storage.GetContext()
storage.Put(c, key, []byte("value"))
}
func Check() {
c := storage.GetContext()
val := storage.Get(c, key)
if val == nil {
panic("value is nil")
}
}
`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
Permissions: []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
})
e.DeployContract(t, ctrA, nil)
ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
ctrInvoker.Invoke(t, stackitem.Null{}, "test")
// A separate call is needed to check whether all VM contexts were properly
// unwrapped and persisted during the previous call.
ctrInvoker.Invoke(t, stackitem.Null{}, "check")
}
// This test is written to check https://github.com/nspcc-dev/neo-go/issues/2509
// and https://github.com/neo-project/neo/pull/2745#discussion_r879167180.
func TestRET_after_FINALLY_PanicInsideVoidMethod(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A throws catchable exception. It also has a non-void method.
srcA := `package contractA
func Panic() {
panic("panic from A")
}
func ReturnSomeValue() int {
return 5
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
})
e.DeployContract(t, ctrA, nil)
var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}
// Contract B calls A and catches the exception thrown by A.
srcB := `package contractB
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
)
func Catch() {
defer func() {
if r := recover(); r != nil {
// Call method with return value to check https://github.com/neo-project/neo/pull/2745#discussion_r879167180.
contract.Call(interop.Hash160{` + hashAStr + `}, "returnSomeValue", contract.All)
}
}()
contract.Call(interop.Hash160{` + hashAStr + `}, "panic", contract.All)
}`
ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Methods: manifest.WildStrings{Value: nil},
},
},
})
e.DeployContract(t, ctrB, nil)
ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
ctrInvoker.Invoke(t, stackitem.Null{}, "catch")
}
// This test is written to check https://github.com/neo-project/neo/pull/2745#discussion_r879125733.
func TestRET_after_FINALLY_CallNonVoidAfterVoidMethod(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A has two methods. One of them has no return value, and the other has it.
srcA := `package contractA
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func NoRet() {
runtime.Log("no ret")
}
func HasRet() int {
runtime.Log("ret")
return 5
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
})
e.DeployContract(t, ctrA, nil)
var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}
// Contract B calls A in try-catch block.
srcB := `package contractB
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
func CallAInTryCatch() {
defer func() {
if r := recover(); r != nil {
util.Abort() // should never happen
}
}()
contract.Call(interop.Hash160{` + hashAStr + `}, "noRet", contract.All)
contract.Call(interop.Hash160{` + hashAStr + `}, "hasRet", contract.All)
}`
ctrB := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
Name: "contractB",
NoEventsCheck: true,
NoPermissionsCheck: true,
Permissions: []manifest.Permission{
{
Methods: manifest.WildStrings{Value: nil},
},
},
})
e.DeployContract(t, ctrB, nil)
ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
h := ctrInvoker.Invoke(t, stackitem.Null{}, "callAInTryCatch")
aer := e.GetTxExecResult(t, h)
require.Equal(t, 1, len(aer.Stack))
}
// This test is created to check https://github.com/neo-project/neo/pull/2755#discussion_r880087983.
func TestCALLL_from_VoidContext(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
// Contract A has void method `CallHasRet` which calls non-void method `HasRet`.
srcA := `package contractA
func CallHasRet() { // Creates a context with non-nil onUnload.
HasRet()
}
func HasRet() int { // CALL_L clones parent context, check that onUnload is not cloned.
return 5
}`
ctrA := neotest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
NoEventsCheck: true,
NoPermissionsCheck: true,
Name: "contractA",
})
e.DeployContract(t, ctrA, nil)
ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
ctrInvoker.Invoke(t, stackitem.Null{}, "callHasRet")
}
func loadScript(ic *interop.Context, script []byte, args ...any) {
ic.SpawnVM()
ic.VM.LoadScriptWithFlags(script, callflag.AllowCall)
for i := range args {
ic.VM.Estack().PushVal(args[i])
}
ic.VM.GasLimit = -1
}
func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...any) {
ic.SpawnVM()
ic.VM.LoadScriptWithHash(script, hash, f)
for i := range args {
ic.VM.Estack().PushVal(args[i])
}
ic.VM.GasLimit = -1
}