mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-29 23:33:37 +00:00
core: move contract-related tests to the contract package
This commit is contained in:
parent
e7e80fda64
commit
f0d7a1da2a
3 changed files with 484 additions and 500 deletions
|
@ -5,7 +5,9 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
@ -13,6 +15,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util/slice"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -113,3 +116,58 @@ func TestCreateMultisigAccount(t *testing.T) {
|
||||||
e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32")
|
e.InvokeScriptCheckFAULT(t, w.Bytes(), []neotest.Signer{acc}, "m must be positive and fit int32")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount_Hardfork(t *testing.T) {
|
||||||
|
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) {
|
||||||
|
c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled
|
||||||
|
c.Hardforks = map[string]uint32{
|
||||||
|
config.HFAspidochelone.String(): 2,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
e := neotest.NewExecutor(t, bc, acc, acc)
|
||||||
|
|
||||||
|
priv, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pub := priv.PublicKey()
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.Array(w.BinWriter, []interface{}{pub.Bytes(), pub.Bytes(), pub.Bytes()}...)
|
||||||
|
emit.Int(w.BinWriter, int64(2))
|
||||||
|
emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount)
|
||||||
|
require.NoError(t, w.Err)
|
||||||
|
multisigScript := slice.Copy(w.Bytes())
|
||||||
|
|
||||||
|
w.Reset()
|
||||||
|
emit.Bytes(w.BinWriter, pub.Bytes())
|
||||||
|
emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount)
|
||||||
|
require.NoError(t, w.Err)
|
||||||
|
standardScript := slice.Copy(w.Bytes())
|
||||||
|
|
||||||
|
createAccTx := func(t *testing.T, script []byte) *transaction.Transaction {
|
||||||
|
tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Committee}, bc.BlockHeight()+1)
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocks #1, #2: old prices
|
||||||
|
tx1Standard := createAccTx(t, standardScript)
|
||||||
|
tx1Multisig := createAccTx(t, multisigScript)
|
||||||
|
e.AddNewBlock(t, tx1Standard, tx1Multisig)
|
||||||
|
e.CheckHalt(t, tx1Standard.Hash())
|
||||||
|
e.CheckHalt(t, tx1Multisig.Hash())
|
||||||
|
tx2Standard := createAccTx(t, standardScript)
|
||||||
|
tx2Multisig := createAccTx(t, multisigScript)
|
||||||
|
e.AddNewBlock(t, tx2Standard, tx2Multisig)
|
||||||
|
e.CheckHalt(t, tx2Standard.Hash())
|
||||||
|
e.CheckHalt(t, tx2Multisig.Hash())
|
||||||
|
|
||||||
|
// block #3: updated prices (larger than the previous ones)
|
||||||
|
tx3Standard := createAccTx(t, standardScript)
|
||||||
|
tx3Multisig := createAccTx(t, multisigScript)
|
||||||
|
e.AddNewBlock(t, tx3Standard, tx3Multisig)
|
||||||
|
e.CheckHalt(t, tx3Standard.Hash())
|
||||||
|
e.CheckHalt(t, tx3Multisig.Hash())
|
||||||
|
require.True(t, tx1Standard.SystemFee == tx2Standard.SystemFee)
|
||||||
|
require.True(t, tx1Multisig.SystemFee == tx2Multisig.SystemFee)
|
||||||
|
require.True(t, tx2Standard.SystemFee < tx3Standard.SystemFee)
|
||||||
|
require.True(t, tx2Multisig.SystemFee < tx3Multisig.SystemFee)
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
package contract_test
|
package contract_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/internal/contracts"
|
"github.com/nspcc-dev/neo-go/internal/contracts"
|
||||||
"github.com/nspcc-dev/neo-go/internal/random"
|
"github.com/nspcc-dev/neo-go/internal/random"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
"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"
|
||||||
|
"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/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/neotest/chain"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
"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/smartcontract/trigger"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"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/opcode"
|
||||||
|
@ -163,6 +170,425 @@ func TestCall(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}}},
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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}}},
|
||||||
|
})
|
||||||
|
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}}},
|
||||||
|
})
|
||||||
|
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.Notify("no ret")
|
||||||
|
}
|
||||||
|
func HasRet() int {
|
||||||
|
runtime.Notify("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 ...interface{}) {
|
func loadScript(ic *interop.Context, script []byte, args ...interface{}) {
|
||||||
ic.SpawnVM()
|
ic.SpawnVM()
|
||||||
ic.VM.LoadScriptWithFlags(script, callflag.AllowCall)
|
ic.VM.LoadScriptWithFlags(script, callflag.AllowCall)
|
||||||
|
|
|
@ -1,500 +0,0 @@
|
||||||
package core_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/internal/contracts"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
||||||
"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/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"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/manifest"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util/slice"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 TestSystemContractCreateAccount_Hardfork(t *testing.T) {
|
|
||||||
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.ProtocolConfiguration) {
|
|
||||||
c.P2PSigExtensions = true // `basicchain.Init` requires Notary enabled
|
|
||||||
c.Hardforks = map[string]uint32{
|
|
||||||
config.HFAspidochelone.String(): 2,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
e := neotest.NewExecutor(t, bc, acc, acc)
|
|
||||||
|
|
||||||
priv, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
pub := priv.PublicKey()
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Array(w.BinWriter, []interface{}{pub.Bytes(), pub.Bytes(), pub.Bytes()}...)
|
|
||||||
emit.Int(w.BinWriter, int64(2))
|
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemContractCreateMultisigAccount)
|
|
||||||
require.NoError(t, w.Err)
|
|
||||||
multisigScript := slice.Copy(w.Bytes())
|
|
||||||
|
|
||||||
w.Reset()
|
|
||||||
emit.Bytes(w.BinWriter, pub.Bytes())
|
|
||||||
emit.Syscall(w.BinWriter, interopnames.SystemContractCreateStandardAccount)
|
|
||||||
require.NoError(t, w.Err)
|
|
||||||
standardScript := slice.Copy(w.Bytes())
|
|
||||||
|
|
||||||
createAccTx := func(t *testing.T, script []byte) *transaction.Transaction {
|
|
||||||
tx := e.PrepareInvocation(t, script, []neotest.Signer{e.Committee}, bc.BlockHeight()+1)
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
|
|
||||||
// blocks #1, #2: old prices
|
|
||||||
tx1Standard := createAccTx(t, standardScript)
|
|
||||||
tx1Multisig := createAccTx(t, multisigScript)
|
|
||||||
e.AddNewBlock(t, tx1Standard, tx1Multisig)
|
|
||||||
e.CheckHalt(t, tx1Standard.Hash())
|
|
||||||
e.CheckHalt(t, tx1Multisig.Hash())
|
|
||||||
tx2Standard := createAccTx(t, standardScript)
|
|
||||||
tx2Multisig := createAccTx(t, multisigScript)
|
|
||||||
e.AddNewBlock(t, tx2Standard, tx2Multisig)
|
|
||||||
e.CheckHalt(t, tx2Standard.Hash())
|
|
||||||
e.CheckHalt(t, tx2Multisig.Hash())
|
|
||||||
|
|
||||||
// block #3: updated prices (larger than the previous ones)
|
|
||||||
tx3Standard := createAccTx(t, standardScript)
|
|
||||||
tx3Multisig := createAccTx(t, multisigScript)
|
|
||||||
e.AddNewBlock(t, tx3Standard, tx3Multisig)
|
|
||||||
e.CheckHalt(t, tx3Standard.Hash())
|
|
||||||
e.CheckHalt(t, tx3Multisig.Hash())
|
|
||||||
require.True(t, tx1Standard.SystemFee == tx2Standard.SystemFee)
|
|
||||||
require.True(t, tx1Multisig.SystemFee == tx2Multisig.SystemFee)
|
|
||||||
require.True(t, tx2Standard.SystemFee < tx3Standard.SystemFee)
|
|
||||||
require.True(t, tx2Multisig.SystemFee < tx3Multisig.SystemFee)
|
|
||||||
}
|
|
||||||
|
|
||||||
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}}},
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
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}}},
|
|
||||||
})
|
|
||||||
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}}},
|
|
||||||
})
|
|
||||||
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.Notify("no ret")
|
|
||||||
}
|
|
||||||
func HasRet() int {
|
|
||||||
runtime.Notify("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")
|
|
||||||
}
|
|
Loading…
Reference in a new issue