Merge pull request #2508 from nspcc-dev/snapshot-isolation
core, vm: implement snapshot isolation
This commit is contained in:
commit
502cdd68ab
12 changed files with 500 additions and 68 deletions
|
@ -2247,7 +2247,7 @@ func (bc *Blockchain) InitVerificationContext(ic *interop.Context, hash util.Uin
|
||||||
}
|
}
|
||||||
ic.Invocations[cs.Hash]++
|
ic.Invocations[cs.Hash]++
|
||||||
ic.VM.LoadNEFMethod(&cs.NEF, util.Uint160{}, hash, callflag.ReadOnly,
|
ic.VM.LoadNEFMethod(&cs.NEF, util.Uint160{}, hash, callflag.ReadOnly,
|
||||||
true, verifyOffset, initOffset)
|
true, verifyOffset, initOffset, nil)
|
||||||
}
|
}
|
||||||
if len(witness.InvocationScript) != 0 {
|
if len(witness.InvocationScript) != 0 {
|
||||||
err := vm.IsScriptCorrect(witness.InvocationScript, nil)
|
err := vm.IsScriptCorrect(witness.InvocationScript, nil)
|
||||||
|
|
|
@ -381,3 +381,12 @@ func (ic *Context) IsHardforkEnabled(hf config.Hardfork) bool {
|
||||||
}
|
}
|
||||||
return len(ic.Hardforks) == 0 // Enable each hard-fork by default.
|
return len(ic.Hardforks) == 0 // Enable each hard-fork by default.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddNotification creates notification event and appends it to the notification list.
|
||||||
|
func (ic *Context) AddNotification(hash util.Uint160, name string, item *stackitem.Array) {
|
||||||
|
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
|
||||||
|
ScriptHash: hash,
|
||||||
|
Name: name,
|
||||||
|
Item: item,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func LoadToken(ic *interop.Context) func(id int32) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
|
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
|
||||||
}
|
}
|
||||||
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args)
|
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +69,11 @@ func Call(ic *interop.Context) error {
|
||||||
return fmt.Errorf("method not found: %s/%d", method, len(args))
|
return fmt.Errorf("method not found: %s/%d", method, len(args))
|
||||||
}
|
}
|
||||||
hasReturn := md.ReturnType != smartcontract.VoidType
|
hasReturn := md.ReturnType != smartcontract.VoidType
|
||||||
if !hasReturn {
|
return callInternal(ic, cs, method, fs, hasReturn, args, !hasReturn)
|
||||||
ic.VM.Estack().PushItem(stackitem.Null{})
|
|
||||||
}
|
|
||||||
return callInternal(ic, cs, method, fs, hasReturn, args)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func callInternal(ic *interop.Context, cs *state.Contract, name string, f callflag.CallFlag,
|
func callInternal(ic *interop.Context, cs *state.Contract, name string, f callflag.CallFlag,
|
||||||
hasReturn bool, args []stackitem.Item) error {
|
hasReturn bool, args []stackitem.Item, pushNullOnUnloading bool) error {
|
||||||
md := cs.Manifest.ABI.GetMethod(name, len(args))
|
md := cs.Manifest.ABI.GetMethod(name, len(args))
|
||||||
if md.Safe {
|
if md.Safe {
|
||||||
f &^= (callflag.WriteStates | callflag.AllowNotify)
|
f &^= (callflag.WriteStates | callflag.AllowNotify)
|
||||||
|
@ -88,12 +85,12 @@ func callInternal(ic *interop.Context, cs *state.Contract, name string, f callfl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn)
|
return callExFromNative(ic, ic.VM.GetCurrentScriptHash(), cs, name, args, f, hasReturn, pushNullOnUnloading, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// callExFromNative calls a contract with flags using the provided calling hash.
|
// callExFromNative calls a contract with flags using the provided calling hash.
|
||||||
func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract,
|
func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract,
|
||||||
name string, args []stackitem.Item, f callflag.CallFlag, hasReturn bool) error {
|
name string, args []stackitem.Item, f callflag.CallFlag, hasReturn bool, pushNullOnUnloading bool, callFromNative bool) error {
|
||||||
for _, nc := range ic.Natives {
|
for _, nc := range ic.Natives {
|
||||||
if nc.Metadata().Name == nativenames.Policy {
|
if nc.Metadata().Name == nativenames.Policy {
|
||||||
var pch = nc.(policyChecker)
|
var pch = nc.(policyChecker)
|
||||||
|
@ -119,8 +116,37 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra
|
||||||
initOff = md.Offset
|
initOff = md.Offset
|
||||||
}
|
}
|
||||||
ic.Invocations[cs.Hash]++
|
ic.Invocations[cs.Hash]++
|
||||||
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, ic.VM.Context().GetCallFlags()&f,
|
f = ic.VM.Context().GetCallFlags() & f
|
||||||
hasReturn, methodOff, initOff)
|
|
||||||
|
wrapped := f&(callflag.All^callflag.ReadOnly) != 0 || // If the method is safe, then it's read-only and doesn't perform storage changes or emit notifications.
|
||||||
|
ic.VM.Context().HasTryBlock() // If the method is not wrapped into try-catch block, then changes should be discarded anyway if exception occurs.
|
||||||
|
baseNtfCount := len(ic.Notifications)
|
||||||
|
baseDAO := ic.DAO
|
||||||
|
if wrapped {
|
||||||
|
ic.DAO = ic.DAO.GetPrivate()
|
||||||
|
}
|
||||||
|
onUnload := func(commit bool) error {
|
||||||
|
if wrapped {
|
||||||
|
if commit {
|
||||||
|
_, err := ic.DAO.Persist()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to persist changes %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ic.Notifications = ic.Notifications[:baseNtfCount] // Rollback all notification changes made by current context.
|
||||||
|
}
|
||||||
|
ic.DAO = baseDAO
|
||||||
|
}
|
||||||
|
if pushNullOnUnloading && commit {
|
||||||
|
ic.VM.Context().Estack().PushItem(stackitem.Null{}) // Must use current context stack.
|
||||||
|
}
|
||||||
|
if callFromNative && !commit {
|
||||||
|
return fmt.Errorf("unhandled exception")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ic.VM.LoadNEFMethod(&cs.NEF, caller, cs.Hash, f,
|
||||||
|
hasReturn, methodOff, initOff, onUnload)
|
||||||
|
|
||||||
for e, i := ic.VM.Estack(), len(args)-1; i >= 0; i-- {
|
for e, i := ic.VM.Estack(), len(args)-1; i >= 0; i-- {
|
||||||
e.PushItem(args[i])
|
e.PushItem(args[i])
|
||||||
|
@ -134,7 +160,7 @@ var ErrNativeCall = errors.New("failed native call")
|
||||||
// CallFromNative performs synchronous call from native contract.
|
// CallFromNative performs synchronous call from native contract.
|
||||||
func CallFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract, method string, args []stackitem.Item, hasReturn bool) error {
|
func CallFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contract, method string, args []stackitem.Item, hasReturn bool) error {
|
||||||
startSize := ic.VM.Istack().Len()
|
startSize := ic.VM.Istack().Len()
|
||||||
if err := callExFromNative(ic, caller, cs, method, args, callflag.All, hasReturn); err != nil {
|
if err := callExFromNative(ic, caller, cs, method, args, callflag.All, hasReturn, false, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"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/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
@ -69,12 +68,7 @@ func Notify(ic *interop.Context) error {
|
||||||
if len(bytes) > MaxNotificationSize {
|
if len(bytes) > MaxNotificationSize {
|
||||||
return fmt.Errorf("notification size shouldn't exceed %d", MaxNotificationSize)
|
return fmt.Errorf("notification size shouldn't exceed %d", MaxNotificationSize)
|
||||||
}
|
}
|
||||||
ne := state.NotificationEvent{
|
ic.AddNotification(ic.VM.GetCurrentScriptHash(), name, stackitem.DeepCopy(stackitem.NewArray(args)).(*stackitem.Array))
|
||||||
ScriptHash: ic.VM.GetCurrentScriptHash(),
|
|
||||||
Name: name,
|
|
||||||
Item: stackitem.DeepCopy(stackitem.NewArray(args)).(*stackitem.Array),
|
|
||||||
}
|
|
||||||
ic.Notifications = append(ic.Notifications, ne)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,14 @@ package core_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"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/pkg/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
"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/interopnames"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
|
@ -19,6 +22,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
"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"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"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"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util/slice"
|
"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"
|
||||||
|
@ -311,3 +315,395 @@ func TestSystemContractCreateAccount_Hardfork(t *testing.T) {
|
||||||
require.True(t, tx2Standard.SystemFee < tx3Standard.SystemFee)
|
require.True(t, tx2Standard.SystemFee < tx3Standard.SystemFee)
|
||||||
require.True(t, tx2Multisig.SystemFee < tx3Multisig.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")
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/stateroot"
|
"github.com/nspcc-dev/neo-go/pkg/core/stateroot"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||||
|
@ -386,14 +385,10 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs
|
||||||
return fmt.Errorf("failed to update Designation role data cache: %w", err)
|
return fmt.Errorf("failed to update Designation role data cache: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
|
ic.AddNotification(s.Hash, DesignationEventName, stackitem.NewArray([]stackitem.Item{
|
||||||
ScriptHash: s.Hash,
|
stackitem.NewBigInteger(big.NewInt(int64(r))),
|
||||||
Name: DesignationEventName,
|
stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
|
||||||
Item: stackitem.NewArray([]stackitem.Item{
|
}))
|
||||||
stackitem.NewBigInteger(big.NewInt(int64(r))),
|
|
||||||
stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -607,12 +607,7 @@ func (m *Management) getNextContractID(d *dao.Simple) (int32, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Management) emitNotification(ic *interop.Context, name string, hash util.Uint160) {
|
func (m *Management) emitNotification(ic *interop.Context, name string, hash util.Uint160) {
|
||||||
ne := state.NotificationEvent{
|
ic.AddNotification(m.Hash, name, stackitem.NewArray([]stackitem.Item{addrToStackItem(&hash)}))
|
||||||
ScriptHash: m.Hash,
|
|
||||||
Name: name,
|
|
||||||
Item: stackitem.NewArray([]stackitem.Item{addrToStackItem(&hash)}),
|
|
||||||
}
|
|
||||||
ic.Notifications = append(ic.Notifications, ne)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkScriptAndMethods(script []byte, methods []manifest.Method) error {
|
func checkScriptAndMethods(script []byte, methods []manifest.Method) error {
|
||||||
|
|
|
@ -165,16 +165,11 @@ func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) {
|
func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) {
|
||||||
ne := state.NotificationEvent{
|
ic.AddNotification(c.Hash, "Transfer", stackitem.NewArray([]stackitem.Item{
|
||||||
ScriptHash: c.Hash,
|
addrToStackItem(from),
|
||||||
Name: "Transfer",
|
addrToStackItem(to),
|
||||||
Item: stackitem.NewArray([]stackitem.Item{
|
stackitem.NewBigInteger(amount),
|
||||||
addrToStackItem(from),
|
}))
|
||||||
addrToStackItem(to),
|
|
||||||
stackitem.NewBigInteger(amount),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
ic.Notifications = append(ic.Notifications, ne)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateAccBalance adds the specified amount to the acc's balance. If requiredBalance
|
// updateAccBalance adds the specified amount to the acc's balance. If requiredBalance
|
||||||
|
|
|
@ -272,15 +272,11 @@ func (o *Oracle) FinishInternal(ic *interop.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrRequestNotFound
|
return ErrRequestNotFound
|
||||||
}
|
}
|
||||||
|
ic.AddNotification(o.Hash, "OracleResponse", stackitem.NewArray([]stackitem.Item{
|
||||||
|
stackitem.Make(resp.ID),
|
||||||
|
stackitem.Make(req.OriginalTxID.BytesBE()),
|
||||||
|
}))
|
||||||
|
|
||||||
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
|
|
||||||
ScriptHash: o.Hash,
|
|
||||||
Name: "OracleResponse",
|
|
||||||
Item: stackitem.NewArray([]stackitem.Item{
|
|
||||||
stackitem.Make(resp.ID),
|
|
||||||
stackitem.Make(req.OriginalTxID.BytesBE()),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
origTx, _, err := ic.DAO.GetTransaction(req.OriginalTxID)
|
origTx, _, err := ic.DAO.GetTransaction(req.OriginalTxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrRequestNotFound
|
return ErrRequestNotFound
|
||||||
|
@ -382,16 +378,12 @@ func (o *Oracle) RequestInternal(ic *interop.Context, url string, filter *string
|
||||||
} else {
|
} else {
|
||||||
filterNotif = stackitem.Null{}
|
filterNotif = stackitem.Null{}
|
||||||
}
|
}
|
||||||
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
|
ic.AddNotification(o.Hash, "OracleRequest", stackitem.NewArray([]stackitem.Item{
|
||||||
ScriptHash: o.Hash,
|
stackitem.Make(id),
|
||||||
Name: "OracleRequest",
|
stackitem.Make(ic.VM.GetCallingScriptHash().BytesBE()),
|
||||||
Item: stackitem.NewArray([]stackitem.Item{
|
stackitem.Make(url),
|
||||||
stackitem.Make(id),
|
filterNotif,
|
||||||
stackitem.Make(ic.VM.GetCallingScriptHash().BytesBE()),
|
}))
|
||||||
stackitem.Make(url),
|
|
||||||
filterNotif,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
req := &state.OracleRequest{
|
req := &state.OracleRequest{
|
||||||
OriginalTxID: o.getOriginalTxID(ic.DAO, ic.Tx),
|
OriginalTxID: o.getOriginalTxID(ic.DAO, ic.Tx),
|
||||||
GasForResponse: gas.Uint64(),
|
GasForResponse: gas.Uint64(),
|
||||||
|
|
|
@ -109,8 +109,9 @@ func (c *ContractInvoker) InvokeWithFeeFail(t testing.TB, message string, sysFee
|
||||||
|
|
||||||
// InvokeFail invokes the method with the args, persists the transaction and checks the error message.
|
// InvokeFail invokes the method with the args, persists the transaction and checks the error message.
|
||||||
// It returns the transaction hash.
|
// It returns the transaction hash.
|
||||||
func (c *ContractInvoker) InvokeFail(t testing.TB, message string, method string, args ...interface{}) {
|
func (c *ContractInvoker) InvokeFail(t testing.TB, message string, method string, args ...interface{}) util.Uint256 {
|
||||||
tx := c.PrepareInvoke(t, method, args...)
|
tx := c.PrepareInvoke(t, method, args...)
|
||||||
c.AddNewBlock(t, tx)
|
c.AddNewBlock(t, tx)
|
||||||
c.CheckFault(t, tx.Hash(), message)
|
c.CheckFault(t, tx.Hash(), message)
|
||||||
|
return tx.Hash()
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,14 @@ type Context struct {
|
||||||
NEF *nef.File
|
NEF *nef.File
|
||||||
// invTree is an invocation tree (or branch of it) for this context.
|
// invTree is an invocation tree (or branch of it) for this context.
|
||||||
invTree *InvocationTree
|
invTree *InvocationTree
|
||||||
|
// onUnload is a callback that should be called after current context unloading
|
||||||
|
// if no exception occurs.
|
||||||
|
onUnload ContextUnloadCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContextUnloadCallback is a callback method used on context unloading from istack.
|
||||||
|
type ContextUnloadCallback func(commit bool) error
|
||||||
|
|
||||||
var errNoInstParam = errors.New("failed to read instruction parameter")
|
var errNoInstParam = errors.New("failed to read instruction parameter")
|
||||||
|
|
||||||
// NewContext returns a new Context object.
|
// NewContext returns a new Context object.
|
||||||
|
@ -316,3 +322,13 @@ func (v *VM) PushContextScriptHash(n int) error {
|
||||||
v.Estack().PushItem(stackitem.NewByteArray(h.BytesBE()))
|
v.Estack().PushItem(stackitem.NewByteArray(h.BytesBE()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) HasTryBlock() bool {
|
||||||
|
for i := 0; i < c.tryStack.Len(); i++ {
|
||||||
|
eCtx := c.tryStack.Peek(i).Value().(*exceptionHandlingContext)
|
||||||
|
if eCtx.State == eTry {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
23
pkg/vm/vm.go
23
pkg/vm/vm.go
|
@ -285,7 +285,7 @@ func (v *VM) LoadScript(b []byte) {
|
||||||
|
|
||||||
// LoadScriptWithFlags loads script and sets call flag to f.
|
// LoadScriptWithFlags loads script and sets call flag to f.
|
||||||
func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
|
func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
|
||||||
v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0)
|
v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), util.Uint160{}, f, -1, 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadScriptWithHash is similar to the LoadScriptWithFlags method, but it also loads
|
// LoadScriptWithHash is similar to the LoadScriptWithFlags method, but it also loads
|
||||||
|
@ -295,19 +295,19 @@ func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
|
||||||
// accordingly). It's up to the user of this function to make sure the script and hash match
|
// accordingly). It's up to the user of this function to make sure the script and hash match
|
||||||
// each other.
|
// each other.
|
||||||
func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) {
|
func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) {
|
||||||
v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), hash, f, 1, 0)
|
v.loadScriptWithCallingHash(b, nil, v.GetCurrentScriptHash(), hash, f, 1, 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadNEFMethod allows to create a context to execute a method from the NEF
|
// LoadNEFMethod allows to create a context to execute a method from the NEF
|
||||||
// file with the specified caller and executing hash, call flags, return value,
|
// file with the specified caller and executing hash, call flags, return value,
|
||||||
// method and _initialize offsets.
|
// method and _initialize offsets.
|
||||||
func (v *VM) LoadNEFMethod(exe *nef.File, caller util.Uint160, hash util.Uint160, f callflag.CallFlag,
|
func (v *VM) LoadNEFMethod(exe *nef.File, caller util.Uint160, hash util.Uint160, f callflag.CallFlag,
|
||||||
hasReturn bool, methodOff int, initOff int) {
|
hasReturn bool, methodOff int, initOff int, onContextUnload ContextUnloadCallback) {
|
||||||
var rvcount int
|
var rvcount int
|
||||||
if hasReturn {
|
if hasReturn {
|
||||||
rvcount = 1
|
rvcount = 1
|
||||||
}
|
}
|
||||||
v.loadScriptWithCallingHash(exe.Script, exe, caller, hash, f, rvcount, methodOff)
|
v.loadScriptWithCallingHash(exe.Script, exe, caller, hash, f, rvcount, methodOff, onContextUnload)
|
||||||
if initOff >= 0 {
|
if initOff >= 0 {
|
||||||
v.Call(initOff)
|
v.Call(initOff)
|
||||||
}
|
}
|
||||||
|
@ -316,7 +316,7 @@ func (v *VM) LoadNEFMethod(exe *nef.File, caller util.Uint160, hash util.Uint160
|
||||||
// loadScriptWithCallingHash is similar to LoadScriptWithHash but sets calling hash explicitly.
|
// loadScriptWithCallingHash is similar to LoadScriptWithHash but sets calling hash explicitly.
|
||||||
// It should be used for calling from native contracts.
|
// It should be used for calling from native contracts.
|
||||||
func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, caller util.Uint160,
|
func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, caller util.Uint160,
|
||||||
hash util.Uint160, f callflag.CallFlag, rvcount int, offset int) {
|
hash util.Uint160, f callflag.CallFlag, rvcount int, offset int, onContextUnload ContextUnloadCallback) {
|
||||||
var sl slot
|
var sl slot
|
||||||
|
|
||||||
v.checkInvocationStackSize()
|
v.checkInvocationStackSize()
|
||||||
|
@ -341,6 +341,7 @@ func (v *VM) loadScriptWithCallingHash(b []byte, exe *nef.File, caller util.Uint
|
||||||
curTree.Calls = append(curTree.Calls, newTree)
|
curTree.Calls = append(curTree.Calls, newTree)
|
||||||
ctx.invTree = newTree
|
ctx.invTree = newTree
|
||||||
}
|
}
|
||||||
|
ctx.onUnload = onContextUnload
|
||||||
v.istack.PushItem(ctx)
|
v.istack.PushItem(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1591,6 +1592,16 @@ func (v *VM) unloadContext(ctx *Context) {
|
||||||
if ctx.static != nil && (currCtx == nil || ctx.static != currCtx.static) {
|
if ctx.static != nil && (currCtx == nil || ctx.static != currCtx.static) {
|
||||||
ctx.static.ClearRefs(&v.refs)
|
ctx.static.ClearRefs(&v.refs)
|
||||||
}
|
}
|
||||||
|
if ctx.onUnload != nil {
|
||||||
|
err := ctx.onUnload(v.uncaughtException == nil)
|
||||||
|
if err != nil {
|
||||||
|
errMessage := fmt.Sprintf("context unload callback failed: %s", err)
|
||||||
|
if v.uncaughtException != nil {
|
||||||
|
errMessage = fmt.Sprintf("%s, uncaught exception: %s", errMessage, v.uncaughtException)
|
||||||
|
}
|
||||||
|
panic(errors.New(errMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTryParams splits TRY(L) instruction parameter into offsets for catch and finally blocks.
|
// getTryParams splits TRY(L) instruction parameter into offsets for catch and finally blocks.
|
||||||
|
@ -1647,6 +1658,8 @@ func (v *VM) call(ctx *Context, offset int) {
|
||||||
newCtx.tryStack.elems = nil
|
newCtx.tryStack.elems = nil
|
||||||
initStack(&newCtx.tryStack, "exception", nil)
|
initStack(&newCtx.tryStack, "exception", nil)
|
||||||
newCtx.NEF = ctx.NEF
|
newCtx.NEF = ctx.NEF
|
||||||
|
// Do not clone unloading callback, new context does not require any actions to perform on unloading.
|
||||||
|
newCtx.onUnload = nil
|
||||||
v.istack.PushItem(newCtx)
|
v.istack.PushItem(newCtx)
|
||||||
newCtx.Jump(offset)
|
newCtx.Jump(offset)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue