mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-27 03:58:06 +00:00
a7aceca74a
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>
743 lines
26 KiB
Go
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
|
|
}
|