github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/vm/json_test.go (about)

     1  package vm
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"math/big"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
    18  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
    19  	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
    20  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    21  	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  type (
    26  	vmUT struct {
    27  		Category string      `json:"category"`
    28  		Name     string      `json:"name"`
    29  		Tests    []vmUTEntry `json:"tests"`
    30  	}
    31  
    32  	vmUTActionType string
    33  
    34  	vmUTEntry struct {
    35  		Name   string
    36  		Script vmUTScript
    37  		Steps  []vmUTStep
    38  	}
    39  
    40  	vmUTExecutionContextState struct {
    41  		Instruction        string          `json:"nextInstruction"`
    42  		InstructionPointer int             `json:"instructionPointer"`
    43  		EStack             []vmUTStackItem `json:"evaluationStack"`
    44  		StaticFields       []vmUTStackItem `json:"staticFields"`
    45  	}
    46  
    47  	vmUTExecutionEngineState struct {
    48  		State           vmstate.State               `json:"state"`
    49  		ResultStack     []vmUTStackItem             `json:"resultStack"`
    50  		InvocationStack []vmUTExecutionContextState `json:"invocationStack"`
    51  	}
    52  
    53  	vmUTScript []byte
    54  
    55  	vmUTStackItem struct {
    56  		Type  vmUTStackItemType
    57  		Value any
    58  	}
    59  
    60  	vmUTStep struct {
    61  		Actions []vmUTActionType         `json:"actions"`
    62  		Result  vmUTExecutionEngineState `json:"result"`
    63  	}
    64  
    65  	vmUTStackItemType string
    66  )
    67  
    68  // stackItemAUX is used as an intermediate structure
    69  // to conditionally unmarshal vmUTStackItem based
    70  // on the value of Type field.
    71  type stackItemAUX struct {
    72  	Type  vmUTStackItemType `json:"type"`
    73  	Value json.RawMessage   `json:"value"`
    74  }
    75  
    76  const (
    77  	vmExecute  vmUTActionType = "execute"
    78  	vmStepInto vmUTActionType = "stepinto"
    79  	vmStepOut  vmUTActionType = "stepout"
    80  	vmStepOver vmUTActionType = "stepover"
    81  
    82  	typeArray      vmUTStackItemType = "array"
    83  	typeBoolean    vmUTStackItemType = "boolean"
    84  	typeBuffer     vmUTStackItemType = "buffer"
    85  	typeByteString vmUTStackItemType = "bytestring"
    86  	typeInteger    vmUTStackItemType = "integer"
    87  	typeInterop    vmUTStackItemType = "interop"
    88  	typeMap        vmUTStackItemType = "map"
    89  	typeNull       vmUTStackItemType = "null"
    90  	typePointer    vmUTStackItemType = "pointer"
    91  	typeString     vmUTStackItemType = "string"
    92  	typeStruct     vmUTStackItemType = "struct"
    93  
    94  	testsDir = "testdata/neo-vm/tests/Neo.VM.Tests/Tests/"
    95  )
    96  
    97  func TestUT(t *testing.T) {
    98  	testsRan := false
    99  	err := filepath.Walk(testsDir, func(path string, info os.FileInfo, err error) error {
   100  		if !strings.HasSuffix(path, ".json") {
   101  			return nil
   102  		}
   103  
   104  		testFile(t, path)
   105  		testsRan = true
   106  		return nil
   107  	})
   108  
   109  	require.NoError(t, err)
   110  	require.Equal(t, true, testsRan, "neo-vm tests should be available (check submodules)")
   111  }
   112  
   113  func testSyscallHandler(v *VM, id uint32) error {
   114  	switch id {
   115  	case 0x77777777:
   116  		v.Estack().PushVal(stackitem.NewInterop(new(int)))
   117  	case 0x66666666:
   118  		if !v.Context().sc.callFlag.Has(callflag.ReadOnly) {
   119  			return errors.New("invalid call flags")
   120  		}
   121  		v.Estack().PushVal(stackitem.NewInterop(new(int)))
   122  	case 0x55555555:
   123  		v.Estack().PushVal(stackitem.NewInterop(new(int)))
   124  	case 0xADDEADDE:
   125  		v.throw(stackitem.Make("error"))
   126  	default:
   127  		return errors.New("syscall not found")
   128  	}
   129  	return nil
   130  }
   131  
   132  func testFile(t *testing.T, filename string) {
   133  	data, err := os.ReadFile(filename)
   134  	require.NoError(t, err)
   135  
   136  	// get rid of possible BOM
   137  	if len(data) > 2 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
   138  		data = data[3:]
   139  	}
   140  	if strings.HasSuffix(filename, "MEMCPY.json") {
   141  		return // FIXME not a valid JSON https://github.com/neo-project/neo-vm/issues/322
   142  	}
   143  
   144  	ut := new(vmUT)
   145  	require.NoErrorf(t, json.Unmarshal(data, ut), "file: %s", filename)
   146  
   147  	t.Run(ut.Category+":"+ut.Name, func(t *testing.T) {
   148  		for i := range ut.Tests {
   149  			test := ut.Tests[i]
   150  			if test.Name == "try catch with syscall exception" {
   151  				continue // FIXME unresolved issue https://github.com/neo-project/neo-vm/issues/343
   152  			}
   153  			t.Run(ut.Tests[i].Name, func(t *testing.T) {
   154  				prog := []byte(test.Script)
   155  				vm := load(prog)
   156  				vm.state = vmstate.Break
   157  				vm.SyscallHandler = testSyscallHandler
   158  
   159  				for i := range test.Steps {
   160  					execStep(t, vm, test.Steps[i])
   161  					result := test.Steps[i].Result
   162  					require.Equal(t, result.State, vm.state)
   163  					if result.State == vmstate.Fault { // do not compare stacks on fault
   164  						continue
   165  					}
   166  
   167  					if len(result.InvocationStack) > 0 {
   168  						for i, s := range result.InvocationStack {
   169  							ctx := vm.istack[len(vm.istack)-1-i]
   170  							if ctx.nextip < len(ctx.sc.prog) {
   171  								require.Equal(t, s.InstructionPointer, ctx.nextip)
   172  								op, err := opcode.FromString(s.Instruction)
   173  								require.NoError(t, err)
   174  								require.Equal(t, op, opcode.Opcode(ctx.sc.prog[ctx.nextip]))
   175  							}
   176  							compareStacks(t, s.EStack, vm.estack)
   177  							compareSlots(t, s.StaticFields, ctx.sc.static)
   178  						}
   179  					}
   180  
   181  					if len(result.ResultStack) != 0 {
   182  						compareStacks(t, result.ResultStack, vm.estack)
   183  					}
   184  				}
   185  			})
   186  		}
   187  	})
   188  }
   189  
   190  func compareItems(t *testing.T, a, b stackitem.Item) {
   191  	switch si := a.(type) {
   192  	case *stackitem.BigInteger:
   193  		val := si.Value().(*big.Int).Int64()
   194  		switch ac := b.(type) {
   195  		case *stackitem.BigInteger:
   196  			require.Equal(t, val, ac.Value().(*big.Int).Int64())
   197  		case *stackitem.ByteArray:
   198  			require.Equal(t, val, bigint.FromBytes(ac.Value().([]byte)).Int64())
   199  		case stackitem.Bool:
   200  			if ac.Value().(bool) {
   201  				require.Equal(t, val, int64(1))
   202  			} else {
   203  				require.Equal(t, val, int64(0))
   204  			}
   205  		default:
   206  			require.Fail(t, "wrong type")
   207  		}
   208  	case *stackitem.Pointer:
   209  		p, ok := b.(*stackitem.Pointer)
   210  		require.True(t, ok)
   211  		require.Equal(t, si.Position(), p.Position()) // there no script in test files
   212  	case *stackitem.Array, *stackitem.Struct:
   213  		require.Equal(t, a.Type(), b.Type())
   214  
   215  		as := a.Value().([]stackitem.Item)
   216  		bs := a.Value().([]stackitem.Item)
   217  		require.Equal(t, len(as), len(bs))
   218  
   219  		for i := range as {
   220  			compareItems(t, as[i], bs[i])
   221  		}
   222  
   223  	case *stackitem.Map:
   224  		require.Equal(t, a.Type(), b.Type())
   225  
   226  		as := a.Value().([]stackitem.MapElement)
   227  		bs := a.Value().([]stackitem.MapElement)
   228  		require.Equal(t, len(as), len(bs))
   229  
   230  		for i := range as {
   231  			compareItems(t, as[i].Key, bs[i].Key)
   232  			compareItems(t, as[i].Value, bs[i].Value)
   233  		}
   234  	default:
   235  		require.Equal(t, a, b)
   236  	}
   237  }
   238  
   239  func compareStacks(t *testing.T, expected []vmUTStackItem, actual *Stack) {
   240  	compareItemArrays(t, expected, actual.Len(), func(i int) stackitem.Item { return actual.Peek(i).Item() })
   241  }
   242  
   243  func compareSlots(t *testing.T, expected []vmUTStackItem, actual slot) {
   244  	if actual == nil && len(expected) == 0 {
   245  		return
   246  	}
   247  	require.NotNil(t, actual)
   248  	compareItemArrays(t, expected, actual.Size(), actual.Get)
   249  }
   250  
   251  func compareItemArrays(t *testing.T, expected []vmUTStackItem, n int, getItem func(i int) stackitem.Item) {
   252  	if expected == nil {
   253  		return
   254  	}
   255  
   256  	require.Equal(t, len(expected), n)
   257  	for i, item := range expected {
   258  		it := getItem(i)
   259  		require.NotNil(t, it)
   260  
   261  		if item.Type == typeInterop {
   262  			require.IsType(t, (*stackitem.Interop)(nil), it)
   263  			continue
   264  		}
   265  		compareItems(t, item.toStackItem(), it)
   266  	}
   267  }
   268  
   269  func (v *vmUTStackItem) toStackItem() stackitem.Item {
   270  	switch v.Type.toLower() {
   271  	case typeArray:
   272  		items := v.Value.([]vmUTStackItem)
   273  		result := make([]stackitem.Item, len(items))
   274  		for i := range items {
   275  			result[i] = items[i].toStackItem()
   276  		}
   277  		return stackitem.NewArray(result)
   278  	case typeString:
   279  		panic("not implemented")
   280  	case typeMap:
   281  		return v.Value.(*stackitem.Map)
   282  	case typeInterop:
   283  		panic("not implemented")
   284  	case typeByteString:
   285  		return stackitem.NewByteArray(v.Value.([]byte))
   286  	case typeBuffer:
   287  		return stackitem.NewBuffer(v.Value.([]byte))
   288  	case typePointer:
   289  		return stackitem.NewPointer(v.Value.(int), nil)
   290  	case typeNull:
   291  		return stackitem.Null{}
   292  	case typeBoolean:
   293  		return stackitem.NewBool(v.Value.(bool))
   294  	case typeInteger:
   295  		return stackitem.NewBigInteger(v.Value.(*big.Int))
   296  	case typeStruct:
   297  		items := v.Value.([]vmUTStackItem)
   298  		result := make([]stackitem.Item, len(items))
   299  		for i := range items {
   300  			result[i] = items[i].toStackItem()
   301  		}
   302  		return stackitem.NewStruct(result)
   303  	default:
   304  		panic(fmt.Sprintf("invalid type: %s", v.Type))
   305  	}
   306  }
   307  
   308  func execStep(t *testing.T, v *VM, step vmUTStep) {
   309  	for i, a := range step.Actions {
   310  		var err error
   311  		switch a.toLower() {
   312  		case vmExecute:
   313  			err = v.Run()
   314  		case vmStepInto:
   315  			err = v.StepInto()
   316  		case vmStepOut:
   317  			err = v.StepOut()
   318  		case vmStepOver:
   319  			err = v.StepOver()
   320  		default:
   321  			panic(fmt.Sprintf("invalid action: %s", a))
   322  		}
   323  
   324  		// only the last action is allowed to fail
   325  		if i+1 < len(step.Actions) {
   326  			require.NoError(t, err)
   327  		}
   328  	}
   329  }
   330  
   331  func jsonStringToInteger(s string) stackitem.Item {
   332  	b, err := decodeHex(s)
   333  	if err == nil {
   334  		return stackitem.NewBigInteger(new(big.Int).SetBytes(b))
   335  	}
   336  	return nil
   337  }
   338  
   339  func (v vmUTStackItemType) toLower() vmUTStackItemType {
   340  	return vmUTStackItemType(strings.ToLower(string(v)))
   341  }
   342  
   343  func (v *vmUTScript) UnmarshalJSON(data []byte) error {
   344  	var ops []string
   345  	if err := json.Unmarshal(data, &ops); err != nil {
   346  		return err
   347  	}
   348  
   349  	var script []byte
   350  	for i := range ops {
   351  		if b, ok := decodeSingle(ops[i]); ok {
   352  			script = append(script, b...)
   353  		} else {
   354  			return fmt.Errorf("invalid script part: %s", ops[i])
   355  		}
   356  	}
   357  
   358  	*v = script
   359  	return nil
   360  }
   361  
   362  func decodeSingle(s string) ([]byte, bool) {
   363  	if op, err := opcode.FromString(s); err == nil {
   364  		return []byte{byte(op)}, true
   365  	}
   366  	b, err := decodeHex(s)
   367  	return b, err == nil
   368  }
   369  
   370  func (v vmUTActionType) toLower() vmUTActionType {
   371  	return vmUTActionType(strings.ToLower(string(v)))
   372  }
   373  
   374  func (v *vmUTActionType) UnmarshalJSON(data []byte) error {
   375  	return json.Unmarshal(data, (*string)(v))
   376  }
   377  
   378  func (v *vmUTStackItem) UnmarshalJSON(data []byte) error {
   379  	var si stackItemAUX
   380  	if err := json.Unmarshal(data, &si); err != nil {
   381  		return err
   382  	}
   383  
   384  	v.Type = si.Type
   385  
   386  	switch typ := si.Type.toLower(); typ {
   387  	case typeArray, typeStruct:
   388  		var a []vmUTStackItem
   389  		if err := json.Unmarshal(si.Value, &a); err != nil {
   390  			return err
   391  		}
   392  		v.Value = a
   393  	case typeInteger, typePointer:
   394  		num := new(big.Int)
   395  		var a int64
   396  		var s string
   397  		if err := json.Unmarshal(si.Value, &a); err == nil {
   398  			num.SetInt64(a)
   399  		} else if err := json.Unmarshal(si.Value, &s); err == nil {
   400  			num.SetString(s, 10)
   401  		} else {
   402  			panic(fmt.Sprintf("invalid integer: %v", si.Value))
   403  		}
   404  		if typ == typePointer {
   405  			v.Value = int(num.Int64())
   406  		} else {
   407  			v.Value = num
   408  		}
   409  	case typeBoolean:
   410  		var b bool
   411  		if err := json.Unmarshal(si.Value, &b); err != nil {
   412  			return err
   413  		}
   414  		v.Value = b
   415  	case typeByteString, typeBuffer:
   416  		b, err := decodeBytes(si.Value)
   417  		if err != nil {
   418  			return err
   419  		}
   420  		v.Value = b
   421  	case typeInterop, typeNull:
   422  		v.Value = nil
   423  	case typeMap:
   424  		// we want to have the same order as in test file, so a custom decoder is used
   425  		d := json.NewDecoder(bytes.NewReader(si.Value))
   426  		if tok, err := d.Token(); err != nil || tok != json.Delim('{') {
   427  			return fmt.Errorf("invalid map start")
   428  		}
   429  
   430  		result := stackitem.NewMap()
   431  		for {
   432  			tok, err := d.Token()
   433  			if err != nil {
   434  				return err
   435  			} else if tok == json.Delim('}') {
   436  				break
   437  			}
   438  			key, ok := tok.(string)
   439  			if !ok {
   440  				return fmt.Errorf("string expected in map key")
   441  			}
   442  
   443  			var it vmUTStackItem
   444  			if err := d.Decode(&it); err != nil {
   445  				return fmt.Errorf("can't decode map value: %w", err)
   446  			}
   447  
   448  			item := jsonStringToInteger(key)
   449  			if item == nil {
   450  				return fmt.Errorf("can't unmarshal Item %s", key)
   451  			}
   452  			result.Add(item, it.toStackItem())
   453  		}
   454  		v.Value = result
   455  	case typeString:
   456  		panic("not implemented")
   457  	default:
   458  		panic(fmt.Sprintf("unknown type: %s", si.Type))
   459  	}
   460  	return nil
   461  }
   462  
   463  // decodeBytes tries to decode bytes from string.
   464  // It tries hex and base64 encodings.
   465  func decodeBytes(data []byte) ([]byte, error) {
   466  	if len(data) == 2 {
   467  		return []byte{}, nil
   468  	}
   469  
   470  	data = data[1 : len(data)-1] // strip quotes
   471  	if b, err := decodeHex(string(data)); err == nil {
   472  		return b, nil
   473  	}
   474  
   475  	r := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(data))
   476  	return io.ReadAll(r)
   477  }
   478  
   479  func decodeHex(s string) ([]byte, error) {
   480  	s = strings.TrimPrefix(s, "0x")
   481  	return hex.DecodeString(s)
   482  }