2022-06-08 13:02:07 +00:00
package contract_test
import (
2022-06-08 16:01:34 +00:00
"encoding/json"
"fmt"
2022-06-08 13:02:07 +00:00
"math/big"
2022-06-08 13:52:30 +00:00
"path/filepath"
2022-06-08 16:01:34 +00:00
"strings"
2022-06-08 13:02:07 +00:00
"testing"
2022-06-08 13:52:30 +00:00
"github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random"
2022-06-08 16:01:34 +00:00
"github.com/nspcc-dev/neo-go/pkg/compiler"
2024-06-01 10:07:03 +00:00
"github.com/nspcc-dev/neo-go/pkg/config"
2022-06-08 13:02:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/block"
2022-06-08 13:52:30 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/interop"
2022-06-08 13:02:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
2022-06-08 13:52:30 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/native"
2022-06-08 16:01:34 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
2022-06-08 13:02:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
2022-06-08 16:01:34 +00:00
"github.com/nspcc-dev/neo-go/pkg/neotest"
2022-06-08 13:02:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
2023-08-10 11:05:32 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
2022-06-08 13:02:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
2022-06-08 16:01:34 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
2022-06-08 13:02:07 +00:00
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
2022-06-08 13:52:30 +00:00
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
2022-06-08 13:02:07 +00:00
"github.com/stretchr/testify/require"
)
2022-06-08 13:52:30 +00:00
var pathToInternalContracts = filepath . Join ( ".." , ".." , ".." , ".." , "internal" , "contracts" )
2022-06-08 13:02:07 +00:00
func TestGetCallFlags ( t * testing . T ) {
bc , _ := chain . NewSingle ( t )
2022-10-06 10:24:57 +00:00
ic , err := bc . GetTestVM ( trigger . Application , & transaction . Transaction { } , & block . Block { } )
require . NoError ( t , err )
2022-06-08 13:02:07 +00:00
ic . VM . LoadScriptWithHash ( [ ] byte { byte ( opcode . RET ) } , util . Uint160 { 1 , 2 , 3 } , callflag . All )
require . NoError ( t , contract . GetCallFlags ( ic ) )
require . Equal ( t , int64 ( callflag . All ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) . ( * big . Int ) . Int64 ( ) )
}
2022-06-08 13:52:30 +00:00
func TestCall ( t * testing . T ) {
bc , _ := chain . NewSingle ( t )
2022-10-06 10:24:57 +00:00
ic , err := bc . GetTestVM ( trigger . Application , & transaction . Transaction { } , & block . Block { } )
require . NoError ( t , err )
2022-06-08 13:52:30 +00:00
cs , currCs := contracts . GetTestContractState ( t , pathToInternalContracts , 4 , 5 , random . Uint160 ( ) ) // sender and IDs are not important for the test
require . NoError ( t , native . PutContractState ( ic . DAO , cs ) )
require . NoError ( t , native . PutContractState ( ic . DAO , currCs ) )
currScript := currCs . NEF . Script
h := cs . Hash
addArgs := stackitem . NewArray ( [ ] stackitem . Item { stackitem . Make ( 1 ) , stackitem . Make ( 2 ) } )
t . Run ( "Good" , func ( t * testing . T ) {
t . Run ( "2 arguments" , func ( t * testing . T ) {
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( addArgs )
ic . VM . Estack ( ) . PushVal ( callflag . All )
ic . VM . Estack ( ) . PushVal ( "add" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . NoError ( t , contract . Call ( ic ) )
require . NoError ( t , ic . VM . Run ( ) )
require . Equal ( t , 2 , ic . VM . Estack ( ) . Len ( ) )
require . Equal ( t , big . NewInt ( 3 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
require . Equal ( t , big . NewInt ( 42 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
} )
t . Run ( "3 arguments" , func ( t * testing . T ) {
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( stackitem . NewArray (
append ( addArgs . Value ( ) . ( [ ] stackitem . Item ) , stackitem . Make ( 3 ) ) ) )
ic . VM . Estack ( ) . PushVal ( callflag . All )
ic . VM . Estack ( ) . PushVal ( "add" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . NoError ( t , contract . Call ( ic ) )
require . NoError ( t , ic . VM . Run ( ) )
require . Equal ( t , 2 , ic . VM . Estack ( ) . Len ( ) )
require . Equal ( t , big . NewInt ( 6 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
require . Equal ( t , big . NewInt ( 42 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
} )
} )
t . Run ( "CallExInvalidFlag" , func ( t * testing . T ) {
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( addArgs )
ic . VM . Estack ( ) . PushVal ( byte ( 0xFF ) )
ic . VM . Estack ( ) . PushVal ( "add" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . Error ( t , contract . Call ( ic ) )
} )
2023-04-03 10:34:24 +00:00
runInvalid := func ( args ... any ) func ( t * testing . T ) {
2022-06-08 13:52:30 +00:00
return func ( t * testing . T ) {
loadScriptWithHashAndFlags ( ic , currScript , h , callflag . All , 42 )
for i := range args {
ic . VM . Estack ( ) . PushVal ( args [ i ] )
}
// interops can both return error and panic,
// we don't care which kind of error has occurred
require . Panics ( t , func ( ) {
err := contract . Call ( ic )
if err != nil {
panic ( err )
}
} )
}
}
t . Run ( "Invalid" , func ( t * testing . T ) {
t . Run ( "Hash" , runInvalid ( addArgs , "add" , h . BytesBE ( ) [ 1 : ] ) )
t . Run ( "MissingHash" , runInvalid ( addArgs , "add" , util . Uint160 { } . BytesBE ( ) ) )
t . Run ( "Method" , runInvalid ( addArgs , stackitem . NewInterop ( "add" ) , h . BytesBE ( ) ) )
t . Run ( "MissingMethod" , runInvalid ( addArgs , "sub" , h . BytesBE ( ) ) )
t . Run ( "DisallowedMethod" , runInvalid ( stackitem . NewArray ( nil ) , "ret7" , h . BytesBE ( ) ) )
t . Run ( "Arguments" , runInvalid ( 1 , "add" , h . BytesBE ( ) ) )
t . Run ( "NotEnoughArguments" , runInvalid (
stackitem . NewArray ( [ ] stackitem . Item { stackitem . Make ( 1 ) } ) , "add" , h . BytesBE ( ) ) )
t . Run ( "TooMuchArguments" , runInvalid (
stackitem . NewArray ( [ ] stackitem . Item {
stackitem . Make ( 1 ) , stackitem . Make ( 2 ) , stackitem . Make ( 3 ) , stackitem . Make ( 4 ) } ) ,
"add" , h . BytesBE ( ) ) )
} )
t . Run ( "ReturnValues" , func ( t * testing . T ) {
t . Run ( "Many" , func ( t * testing . T ) {
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( stackitem . NewArray ( nil ) )
ic . VM . Estack ( ) . PushVal ( callflag . All )
ic . VM . Estack ( ) . PushVal ( "invalidReturn" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . NoError ( t , contract . Call ( ic ) )
require . Error ( t , ic . VM . Run ( ) )
} )
t . Run ( "Void" , func ( t * testing . T ) {
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( stackitem . NewArray ( nil ) )
ic . VM . Estack ( ) . PushVal ( callflag . All )
ic . VM . Estack ( ) . PushVal ( "justReturn" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . NoError ( t , contract . Call ( ic ) )
require . NoError ( t , ic . VM . Run ( ) )
require . Equal ( t , 2 , ic . VM . Estack ( ) . Len ( ) )
require . Equal ( t , stackitem . Null { } , ic . VM . Estack ( ) . Pop ( ) . Item ( ) )
require . Equal ( t , big . NewInt ( 42 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
} )
} )
t . Run ( "IsolatedStack" , func ( t * testing . T ) {
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( stackitem . NewArray ( nil ) )
ic . VM . Estack ( ) . PushVal ( callflag . All )
ic . VM . Estack ( ) . PushVal ( "drop" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . NoError ( t , contract . Call ( ic ) )
require . Error ( t , ic . VM . Run ( ) )
} )
t . Run ( "CallInitialize" , func ( t * testing . T ) {
t . Run ( "Directly" , runInvalid ( stackitem . NewArray ( [ ] stackitem . Item { } ) , "_initialize" , h . BytesBE ( ) ) )
loadScript ( ic , currScript , 42 )
ic . VM . Estack ( ) . PushVal ( stackitem . NewArray ( [ ] stackitem . Item { stackitem . Make ( 5 ) } ) )
ic . VM . Estack ( ) . PushVal ( callflag . All )
ic . VM . Estack ( ) . PushVal ( "add3" )
ic . VM . Estack ( ) . PushVal ( h . BytesBE ( ) )
require . NoError ( t , contract . Call ( ic ) )
require . NoError ( t , ic . VM . Run ( ) )
require . Equal ( t , 2 , ic . VM . Estack ( ) . Len ( ) )
require . Equal ( t , big . NewInt ( 8 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
require . Equal ( t , big . NewInt ( 42 ) , ic . VM . Estack ( ) . Pop ( ) . Value ( ) )
} )
}
2024-06-01 10:07:03 +00:00
func TestSystemContractCall_Permissions ( t * testing . T ) {
check := func ( t * testing . T , cfg func ( * config . Blockchain ) , shouldUpdateFail bool ) {
bc , acc := chain . NewSingleWithCustomConfig ( t , cfg )
e := neotest . NewExecutor ( t , bc , acc , acc )
// Contract A has an unsafe method.
srcA := ` package contractA
func RetOne ( ) int {
return 1
} `
ctrA := neotest . CompileSource ( t , acc . ScriptHash ( ) , strings . NewReader ( srcA ) , & compiler . Options {
NoEventsCheck : true ,
NoPermissionsCheck : true ,
Name : "contractA" ,
} )
e . DeployContract ( t , ctrA , nil )
var hashAStr string
for i := 0 ; i < util . Uint160Size ; i ++ {
hashAStr += fmt . Sprintf ( "%#x" , ctrA . Hash [ i ] )
if i != util . Uint160Size - 1 {
hashAStr += ", "
}
}
// Contract B has a method that calls contract's A and another update method that
// calls contract's A after B's update.
srcB := ` package contractB
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
)
func CallRetOne ( ) int {
res := contract . Call ( interop . Hash160 { ` + hashAStr + ` } , "retOne" , contract . All ) . ( int )
return res
}
func Update ( nef [ ] byte , manifest [ ] byte ) int {
management . Update ( nef , manifest )
res := contract . Call ( interop . Hash160 { ` + hashAStr + ` } , "retOne" , contract . All ) . ( int )
return res
} `
ctrB := neotest . CompileSource ( t , acc . ScriptHash ( ) , strings . NewReader ( srcB ) , & compiler . Options {
Name : "contractB" ,
NoEventsCheck : true ,
NoPermissionsCheck : true ,
Permissions : [ ] manifest . Permission {
{
Contract : manifest . PermissionDesc { Type : manifest . PermissionHash , Value : ctrA . Hash } ,
Methods : manifest . WildStrings { Value : [ ] string { "retOne" } } ,
} ,
{
Methods : manifest . WildStrings { Value : [ ] string { "update" } } ,
} ,
} ,
} )
e . DeployContract ( t , ctrB , nil )
ctrBInvoker := e . ValidatorInvoker ( ctrB . Hash )
// ctrBUpdated differs from ctrB in that it has no permission to call retOne method of ctrA
ctrBUpdated := neotest . CompileSource ( t , acc . ScriptHash ( ) , strings . NewReader ( srcB ) , & compiler . Options {
Name : "contractB" ,
NoEventsCheck : true ,
NoPermissionsCheck : true ,
Permissions : [ ] manifest . Permission {
{
Contract : manifest . PermissionDesc { Type : manifest . PermissionHash , Value : ctrA . Hash } ,
Methods : manifest . WildStrings { Value : [ ] string { } } ,
} ,
{
Methods : manifest . WildStrings { Value : [ ] string { "update" } } ,
} ,
} ,
} )
// Call to A before B update should HALT.
ctrBInvoker . Invoke ( t , stackitem . Make ( 1 ) , "callRetOne" )
// Call to A in the same context as B update should HALT.
n , err := ctrBUpdated . NEF . Bytes ( )
require . NoError ( t , err )
m , err := json . Marshal ( ctrBUpdated . Manifest )
require . NoError ( t , err )
if shouldUpdateFail {
ctrBInvoker . InvokeFail ( t , "System.Contract.Call failed: disallowed method call" , "update" , n , m )
} else {
ctrBInvoker . Invoke ( t , stackitem . Make ( 1 ) , "update" , n , m )
}
// If contract is updated, then all to A after B update should FAULT (manifest
// is updated, no permission to call retOne method).
if ! shouldUpdateFail {
ctrBInvoker . InvokeFail ( t , "System.Contract.Call failed: disallowed method call" , "callRetOne" )
}
}
// Pre-Domovoi behaviour: an updated contract state is used for permissions check.
check ( t , func ( cfg * config . Blockchain ) {
cfg . Hardforks = map [ string ] uint32 {
config . HFDomovoi . String ( ) : 100500 ,
}
} , true )
// Post-Domovoi behaviour: an executing contract state is used for permissions check.
check ( t , func ( cfg * config . Blockchain ) {
cfg . Hardforks = map [ string ] uint32 {
config . HFDomovoi . String ( ) : 0 ,
}
} , false )
}
2022-06-08 16:01:34 +00:00
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 } } } ,
2023-08-10 11:05:32 +00:00
ContractEvents : [ ] compiler . HybridEvent {
{ Name : "NotificationFromA" , Parameters : [ ] compiler . HybridParameter { { Parameter : manifest . Parameter { Name : "i" , Type : smartcontract . IntegerType } } } } ,
} ,
2022-06-08 16:01:34 +00:00
} )
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 )
}
2023-06-29 08:18:30 +00:00
internalCaller ( keyA , valueA , nNtfA )
}
func internalCaller ( keyA , valueA [ ] byte , nNtfA int ) {
2022-06-08 16:01:34 +00:00
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 } } } ,
2023-08-10 11:05:32 +00:00
ContractEvents : [ ] compiler . HybridEvent {
{ Name : "NotificationFromB before panic" , Parameters : [ ] compiler . HybridParameter { { Parameter : manifest . Parameter { Name : "i" , Type : smartcontract . IntegerType } } } } ,
{ Name : "NotificationFromB after panic" , Parameters : [ ] compiler . HybridParameter { { Parameter : manifest . Parameter { Name : "i" , Type : smartcontract . IntegerType } } } } ,
} ,
2022-06-08 16:01:34 +00:00
} )
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 } } } ,
2023-08-10 11:05:32 +00:00
ContractEvents : [ ] compiler . HybridEvent {
{ Name : "Calling A" } ,
{ Name : "Finish" } ,
{ Name : "Caught" } ,
{ Name : "A" } ,
{ Name : "Unreachable A" } ,
{ Name : "B" } ,
{ Name : "Unreachable B" } ,
{ Name : "C" } ,
} ,
2022-06-08 16:01:34 +00:00
} )
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 ( ) {
2023-08-10 11:05:32 +00:00
runtime . Log ( "no ret" )
2022-06-08 16:01:34 +00:00
}
func HasRet ( ) int {
2023-08-10 11:05:32 +00:00
runtime . Log ( "ret" )
2022-06-08 16:01:34 +00:00
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" )
}
2023-04-03 10:34:24 +00:00
func loadScript ( ic * interop . Context , script [ ] byte , args ... any ) {
2022-06-08 13:52:30 +00:00
ic . SpawnVM ( )
ic . VM . LoadScriptWithFlags ( script , callflag . AllowCall )
for i := range args {
ic . VM . Estack ( ) . PushVal ( args [ i ] )
}
ic . VM . GasLimit = - 1
}
2023-04-03 10:34:24 +00:00
func loadScriptWithHashAndFlags ( ic * interop . Context , script [ ] byte , hash util . Uint160 , f callflag . CallFlag , args ... any ) {
2022-06-08 13:52:30 +00:00
ic . SpawnVM ( )
ic . VM . LoadScriptWithHash ( script , hash , f )
for i := range args {
ic . VM . Estack ( ) . PushVal ( args [ i ] )
}
ic . VM . GasLimit = - 1
}