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 }