neo-go/pkg/core/interop/runtime/ext_test.go
Anna Shaleva 3b11f98cd0 core: review usages of (*intero.Context).BlockHeight method
This method returns persisted block height and doesn't take into account
persisting block height. Some of the callers of this method relay on
the wrong assumption that BlockHeight() returns persisting block index.

Fix improper usages of this method and adjust tests. Ref.
61a066583e/src/Neo/SmartContract/ApplicationEngine.cs (L634).

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-11-21 13:46:13 +03:00

757 lines
26 KiB
Go

package runtime_test
import (
"encoding/json"
"math"
"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/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core"
"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/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"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/state"
"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/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"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"
"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/nef"
"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"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"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 getSharpTestTx(sender util.Uint160) *transaction.Transaction {
tx := transaction.New([]byte{byte(opcode.PUSH2)}, 0)
tx.Nonce = 0
tx.Signers = append(tx.Signers, transaction.Signer{
Account: sender,
Scopes: transaction.CalledByEntry,
})
tx.Attributes = []transaction.Attribute{}
tx.Scripts = append(tx.Scripts, transaction.Witness{InvocationScript: []byte{}, VerificationScript: []byte{}})
return tx
}
func getSharpTestGenesis(t *testing.T) *block.Block {
const configPath = "../../../../config"
cfg, err := config.Load(configPath, netmode.MainNet)
require.NoError(t, err)
b, err := core.CreateGenesisBlock(cfg.ProtocolConfiguration)
require.NoError(t, err)
return b
}
func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) {
chain, _ := chain.NewSingle(t)
ic, err := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
require.NoError(t, err)
v := ic.SpawnVM()
return v, ic, chain
}
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
}
func wrapDynamicScript(t *testing.T, script []byte, flags callflag.CallFlag, args ...any) []byte {
b := io.NewBufBinWriter()
// Params.
emit.Array(b.BinWriter, args...)
emit.Int(b.BinWriter, int64(flags))
emit.Bytes(b.BinWriter, script)
// Wrapped syscall.
emit.Instruction(b.BinWriter, opcode.TRY, []byte{3 + 5 + 2, 0}) // 3
emit.Syscall(b.BinWriter, interopnames.SystemRuntimeLoadScript) // 5
emit.Instruction(b.BinWriter, opcode.ENDTRY, []byte{1 + 11 + 2}) // 2
// Catch block
emit.Opcodes(b.BinWriter, opcode.DROP)
emit.String(b.BinWriter, "exception") // 1 + 1 + 9 == 11 bytes
emit.Opcodes(b.BinWriter, opcode.RET)
require.NoError(t, b.Err)
return b.Bytes()
}
func getDeployedInternal(t *testing.T) (*neotest.Executor, neotest.Signer, *core.Blockchain, *state.Contract) {
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())
return e, acc, bc, cs
}
func TestBurnGas(t *testing.T) {
e, acc, _, cs := getDeployedInternal(t)
cInvoker := e.ValidatorInvoker(cs.Hash)
t.Run("good", func(t *testing.T) {
h := cInvoker.Invoke(t, stackitem.Null{}, "burnGas", int64(1))
res := e.GetTxExecResult(t, h)
t.Run("gas limit exceeded", func(t *testing.T) {
tx := e.NewUnsignedTx(t, cs.Hash, "burnGas", int64(2))
e.SignTx(t, tx, res.GasConsumed, acc)
e.AddNewBlock(t, tx)
e.CheckFault(t, tx.Hash(), "GAS limit exceeded")
})
})
t.Run("too big integer", func(t *testing.T) {
gas := big.NewInt(math.MaxInt64)
gas.Add(gas, big.NewInt(1))
cInvoker.InvokeFail(t, "invalid GAS value", "burnGas", gas)
})
t.Run("zero GAS", func(t *testing.T) {
cInvoker.InvokeFail(t, "GAS must be positive", "burnGas", int64(0))
})
}
func TestCheckWitness(t *testing.T) {
_, ic, _ := createVM(t)
script := []byte{byte(opcode.RET)}
scriptHash := hash.Hash160(script)
check := func(t *testing.T, ic *interop.Context, arg any, shouldFail bool, expected ...bool) {
ic.VM.Estack().PushVal(arg)
err := runtime.CheckWitness(ic)
if shouldFail {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, expected)
actual, ok := ic.VM.Estack().Pop().Value().(bool)
require.True(t, ok)
require.Equal(t, expected[0], actual)
}
}
t.Run("error", func(t *testing.T) {
t.Run("not a hash or key", func(t *testing.T) {
check(t, ic, []byte{1, 2, 3}, true)
})
t.Run("script container is not a transaction", func(t *testing.T) {
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
check(t, ic, random.Uint160().BytesBE(), true)
})
t.Run("check scope", func(t *testing.T) {
t.Run("CustomGroups, missing ReadStates flag", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.CustomGroups,
AllowedGroups: []*keys.PublicKey{},
},
},
}
ic.Tx = tx
callingScriptHash := scriptHash
loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All)
ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.AllowCall)
check(t, ic, hash.BytesBE(), true)
})
t.Run("Rules, missing ReadStates flag", func(t *testing.T) {
hash := random.Uint160()
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.Rules,
Rules: []transaction.WitnessRule{{
Action: transaction.WitnessAllow,
Condition: (*transaction.ConditionGroup)(pk.PublicKey()),
}},
},
},
}
ic.Tx = tx
callingScriptHash := scriptHash
loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All)
ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.AllowCall)
check(t, ic, hash.BytesBE(), true)
})
})
})
t.Run("positive", func(t *testing.T) {
t.Run("calling scripthash", func(t *testing.T) {
t.Run("hashed witness", func(t *testing.T) {
callingScriptHash := scriptHash
loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All)
ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.All)
check(t, ic, callingScriptHash.BytesBE(), false, true)
})
t.Run("keyed witness", func(t *testing.T) {
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
callingScriptHash := pk.PublicKey().GetScriptHash()
loadScriptWithHashAndFlags(ic, script, callingScriptHash, callflag.All)
ic.VM.LoadScriptWithHash([]byte{0x1}, random.Uint160(), callflag.All)
check(t, ic, pk.PublicKey().Bytes(), false, true)
})
})
t.Run("check scope", func(t *testing.T) {
t.Run("Global", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.Global,
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, true)
})
t.Run("CalledByEntry", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.CalledByEntry,
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, true)
})
t.Run("CustomContracts", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.CustomContracts,
AllowedContracts: []util.Uint160{scriptHash},
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, true)
})
t.Run("CustomGroups", func(t *testing.T) {
t.Run("unknown scripthash", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.CustomGroups,
AllowedGroups: []*keys.PublicKey{},
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, false)
})
t.Run("positive", func(t *testing.T) {
targetHash := random.Uint160()
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: targetHash,
Scopes: transaction.CustomGroups,
AllowedGroups: []*keys.PublicKey{pk.PublicKey()},
},
},
}
contractScript := []byte{byte(opcode.PUSH1), byte(opcode.RET)}
contractScriptHash := hash.Hash160(contractScript)
ne, err := nef.NewFile(contractScript)
require.NoError(t, err)
contractState := &state.Contract{
ContractBase: state.ContractBase{
ID: 15,
Hash: contractScriptHash,
NEF: *ne,
Manifest: manifest.Manifest{
Groups: []manifest.Group{{PublicKey: pk.PublicKey(), Signature: make([]byte, keys.SignatureLen)}},
},
},
}
require.NoError(t, native.PutContractState(ic.DAO, contractState))
loadScriptWithHashAndFlags(ic, contractScript, contractScriptHash, callflag.All)
ic.Tx = tx
check(t, ic, targetHash.BytesBE(), false, true)
})
})
t.Run("Rules", func(t *testing.T) {
t.Run("no match", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.Rules,
Rules: []transaction.WitnessRule{{
Action: transaction.WitnessAllow,
Condition: (*transaction.ConditionScriptHash)(&hash),
}},
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, false)
})
t.Run("allow", func(t *testing.T) {
hash := random.Uint160()
var cond = true
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.Rules,
Rules: []transaction.WitnessRule{{
Action: transaction.WitnessAllow,
Condition: (*transaction.ConditionBoolean)(&cond),
}},
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, true)
})
t.Run("deny", func(t *testing.T) {
hash := random.Uint160()
var cond = true
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.Rules,
Rules: []transaction.WitnessRule{{
Action: transaction.WitnessDeny,
Condition: (*transaction.ConditionBoolean)(&cond),
}},
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, false)
})
})
t.Run("bad scope", func(t *testing.T) {
hash := random.Uint160()
tx := &transaction.Transaction{
Signers: []transaction.Signer{
{
Account: hash,
Scopes: transaction.None,
},
},
}
loadScriptWithHashAndFlags(ic, script, scriptHash, callflag.ReadStates)
ic.Tx = tx
check(t, ic, hash.BytesBE(), false, false)
})
})
})
}
func TestLoadScript(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
t.Run("no ret val", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.RET)}, callflag.All)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Null{})
})
t.Run("empty script", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{}, callflag.All)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Null{})
})
t.Run("bad script", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{0xff}, callflag.All)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "invalid script")
})
t.Run("ret val, no params", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.PUSH1)}, callflag.All)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make(1))
})
t.Run("ret val with params", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.MUL)}, callflag.All, 2, 2)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make(4))
})
t.Run("two retrun values", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.PUSH1), byte(opcode.PUSH1)}, callflag.All, 2, 2)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "multiple return values in a cross-contract call")
})
t.Run("invalid flags", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.MUL)}, callflag.CallFlag(0xff), 2, 2)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "call flags out of range")
})
t.Run("abort", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.ABORT)}, callflag.All)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "ABORT")
})
t.Run("internal call", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(e.NativeHash(t, nativenames.Gas), "decimals")
require.NoError(t, err)
script = wrapDynamicScript(t, script, callflag.ReadOnly)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make(8))
})
t.Run("forbidden internal call", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(e.NativeHash(t, nativenames.Neo), "decimals")
require.NoError(t, err)
script = wrapDynamicScript(t, script, callflag.ReadStates)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "missing call flags")
})
t.Run("internal state-changing call", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(e.NativeHash(t, nativenames.Neo), "transfer", acc.ScriptHash(), acc.ScriptHash(), 1, nil)
require.NoError(t, err)
script = wrapDynamicScript(t, script, callflag.All)
e.InvokeScriptCheckFAULT(t, script, []neotest.Signer{acc}, "missing call flags")
})
t.Run("exception", func(t *testing.T) {
script := wrapDynamicScript(t, []byte{byte(opcode.PUSH1), byte(opcode.THROW)}, callflag.ReadOnly)
e.InvokeScriptCheckHALT(t, script, []neotest.Signer{acc}, stackitem.Make("exception"))
})
}
func TestGasLeft(t *testing.T) {
const runtimeGasLeftPrice = 1 << 4
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
w := io.NewBufBinWriter()
gasLimit := 1100
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft)
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGasLeft)
require.NoError(t, w.Err)
tx := transaction.New(w.Bytes(), int64(gasLimit))
tx.Nonce = neotest.Nonce()
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
e.SignTx(t, tx, int64(gasLimit), acc)
e.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash())
res := e.GetTxExecResult(t, tx.Hash())
l1 := res.Stack[0].Value().(*big.Int)
l2 := res.Stack[1].Value().(*big.Int)
require.Equal(t, int64(gasLimit-runtimeGasLeftPrice*interop.DefaultBaseExecFee), l1.Int64())
require.Equal(t, int64(gasLimit-2*runtimeGasLeftPrice*interop.DefaultBaseExecFee), l2.Int64())
}
func TestGetAddressVersion(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
w := io.NewBufBinWriter()
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetAddressVersion)
require.NoError(t, w.Err)
e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(address.NEO3Prefix))))
}
func TestGetInvocationCounter(t *testing.T) {
v, ic, _ := createVM(t)
cs, _ := 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))
ic.Invocations[hash.Hash160([]byte{2})] = 42
t.Run("No invocations", func(t *testing.T) {
v.Load([]byte{1})
// do not return an error in this case.
require.NoError(t, runtime.GetInvocationCounter(ic))
require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64())
})
t.Run("NonZero", func(t *testing.T) {
v.Load([]byte{2})
require.NoError(t, runtime.GetInvocationCounter(ic))
require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64())
})
t.Run("Contract", func(t *testing.T) {
script, err := smartcontract.CreateCallScript(cs.Hash, "invocCounter")
require.NoError(t, err)
v.LoadWithFlags(script, callflag.All)
require.NoError(t, v.Run())
require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64())
})
}
func TestGetNetwork(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
w := io.NewBufBinWriter()
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetNetwork)
require.NoError(t, w.Err)
e.InvokeScriptCheckHALT(t, w.Bytes(), []neotest.Signer{acc}, stackitem.NewBigInteger(big.NewInt(int64(bc.GetConfig().Magic))))
}
func TestGetNotifications(t *testing.T) {
v, ic, _ := createVM(t)
ic.Notifications = []state.NotificationEvent{
{ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{11})})},
{ScriptHash: util.Uint160{2}, Name: "Event2", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{22})})},
{ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{33})})},
}
t.Run("NoFilter", func(t *testing.T) {
v.Estack().PushVal(stackitem.Null{})
require.NoError(t, runtime.GetNotifications(ic))
arr := v.Estack().Pop().Array()
require.Equal(t, len(ic.Notifications), len(arr))
for i := range arr {
elem := arr[i].Value().([]stackitem.Item)
require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value())
name, err := stackitem.ToString(elem[1])
require.NoError(t, err)
require.Equal(t, ic.Notifications[i].Name, name)
ic.Notifications[i].Item.MarkAsReadOnly() // tiny hack for test to be able to compare object references.
require.Equal(t, ic.Notifications[i].Item, elem[2])
}
})
t.Run("WithFilter", func(t *testing.T) {
h := util.Uint160{2}.BytesBE()
v.Estack().PushVal(h)
require.NoError(t, runtime.GetNotifications(ic))
arr := v.Estack().Pop().Array()
require.Equal(t, 1, len(arr))
elem := arr[0].Value().([]stackitem.Item)
require.Equal(t, h, elem[0].Value())
name, err := stackitem.ToString(elem[1])
require.NoError(t, err)
require.Equal(t, ic.Notifications[1].Name, name)
require.Equal(t, ic.Notifications[1].Item, elem[2])
})
}
func TestGetRandom_DifferentTransactions(t *testing.T) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
w := io.NewBufBinWriter()
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeGetRandom)
require.NoError(t, w.Err)
script := w.Bytes()
tx1 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1)
tx2 := e.PrepareInvocation(t, script, []neotest.Signer{e.Validator}, bc.BlockHeight()+1)
e.AddNewBlock(t, tx1, tx2)
e.CheckHalt(t, tx1.Hash())
e.CheckHalt(t, tx2.Hash())
res1 := e.GetTxExecResult(t, tx1.Hash())
res2 := e.GetTxExecResult(t, tx2.Hash())
r1, err := res1.Stack[0].TryInteger()
require.NoError(t, err)
r2, err := res2.Stack[0].TryInteger()
require.NoError(t, err)
require.NotEqual(t, r1, r2)
}
// Tests are taken from
// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs
func TestGetRandomCompatibility(t *testing.T) {
bc, _ := chain.NewSingle(t)
b := getSharpTestGenesis(t)
tx := getSharpTestTx(util.Uint160{})
ic, err := bc.GetTestVM(trigger.Application, tx, b)
require.NoError(t, err)
ic.Network = 860833102 // Old mainnet magic used by C# tests.
ic.VM = vm.New()
ic.VM.LoadScript([]byte{0x01})
ic.VM.GasLimit = 1100_00000000
require.NoError(t, runtime.GetRandom(ic))
require.Equal(t, "271339657438512451304577787170704246350", ic.VM.Estack().Pop().BigInt().String())
require.NoError(t, runtime.GetRandom(ic))
require.Equal(t, "98548189559099075644778613728143131367", ic.VM.Estack().Pop().BigInt().String())
require.NoError(t, runtime.GetRandom(ic))
require.Equal(t, "247654688993873392544380234598471205121", ic.VM.Estack().Pop().BigInt().String())
require.NoError(t, runtime.GetRandom(ic))
require.Equal(t, "291082758879475329976578097236212073607", ic.VM.Estack().Pop().BigInt().String())
require.NoError(t, runtime.GetRandom(ic))
require.Equal(t, "247152297361212656635216876565962360375", ic.VM.Estack().Pop().BigInt().String())
}
func TestNotify(t *testing.T) {
caller := random.Uint160()
newIC := func(name string, args any) *interop.Context {
_, _, bc, cs := getDeployedInternal(t)
ic, err := bc.GetTestVM(trigger.Application, nil, nil)
require.NoError(t, err)
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, callflag.NoneFlag, true, 0, -1, nil)
ic.VM.Estack().PushVal(args)
ic.VM.Estack().PushVal(name)
return ic
}
t.Run("big name", func(t *testing.T) {
ic := newIC(string(make([]byte, runtime.MaxEventNameLen+1)), stackitem.NewArray([]stackitem.Item{stackitem.Null{}}))
err := runtime.Notify(ic)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "event name must be less than 32"), err)
})
t.Run("dynamic script", func(t *testing.T) {
ic := newIC("some", stackitem.Null{})
ic.VM.LoadScriptWithHash([]byte{1}, random.Uint160(), callflag.NoneFlag)
ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(42)}))
ic.VM.Estack().PushVal("event")
err := runtime.Notify(ic)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "notifications are not allowed in dynamic scripts"), err)
})
t.Run("recursive struct", func(t *testing.T) {
arr := stackitem.NewArray([]stackitem.Item{stackitem.Null{}})
arr.Append(arr)
ic := newIC("event", stackitem.NewArray([]stackitem.Item{arr})) // upper array is needed to match manifest event signature.
err := runtime.Notify(ic)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "bad notification: recursive item"), err)
})
t.Run("big notification", func(t *testing.T) {
bs := stackitem.NewByteArray(make([]byte, runtime.MaxNotificationSize+1))
arr := stackitem.NewArray([]stackitem.Item{bs})
ic := newIC("event", arr)
err := runtime.Notify(ic)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "notification size shouldn't exceed 1024"), err)
})
t.Run("good", func(t *testing.T) {
arr := stackitem.NewArray([]stackitem.Item{stackitem.Make(42)})
ic := newIC("event", arr)
require.NoError(t, runtime.Notify(ic))
require.Equal(t, 1, len(ic.Notifications))
arr.MarkAsReadOnly() // tiny hack for test to be able to compare object references.
ev := ic.Notifications[0]
require.Equal(t, "event", ev.Name)
require.Equal(t, ic.VM.GetCurrentScriptHash(), ev.ScriptHash)
require.Equal(t, arr, ev.Item)
// Check deep copy.
arr.Value().([]stackitem.Item)[0] = stackitem.Null{}
require.NotEqual(t, arr, ev.Item)
})
}
func TestSystemRuntimeNotify_HFBasilisk(t *testing.T) {
const (
ntfName = "Hello, world!"
enabledHeight = 3
)
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) {
c.Hardforks = map[string]uint32{
config.HFBasilisk.String(): enabledHeight,
}
})
e := neotest.NewExecutor(t, bc, acc, acc)
script := io.NewBufBinWriter()
emit.Array(script.BinWriter, stackitem.Make(true)) // Boolean instead of Integer declared in manifest
emit.String(script.BinWriter, ntfName)
emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify)
require.NoError(t, script.Err)
ne, err := nef.NewFile(script.Bytes())
require.NoError(t, err)
m := &manifest.Manifest{
Name: "ctr",
ABI: manifest.ABI{
Methods: []manifest.Method{
{
Name: "main",
Offset: 0,
ReturnType: smartcontract.VoidType,
},
},
Events: []manifest.Event{
{
Name: ntfName,
Parameters: []manifest.Parameter{
{
Name: "int",
Type: smartcontract.IntegerType,
},
},
},
},
},
}
ctr := &neotest.Contract{
Hash: state.CreateContractHash(e.Validator.ScriptHash(), ne.Checksum, m.Name),
NEF: ne,
Manifest: m,
}
ctrInv := e.NewInvoker(ctr.Hash, e.Validator)
// Block 0 is genesis.
// Block 1: deploy contract.
e.DeployContract(t, ctr, nil)
// Block 2: bad event should be logged.
ctrInv.Invoke(t, nil, "main")
// Block 3: bad event should fault the execution.
require.Equal(t, uint32(enabledHeight-1), e.Chain.BlockHeight())
ctrInv.InvokeFail(t,
"System.Runtime.Notify failed: notification Hello, world! is invalid: parameter 0 type mismatch: Integer (manifest) vs Boolean (notification)",
"main")
}