diff --git a/docs/notifications.md b/docs/notifications.md index 140d2403e..9e8426ab7 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -293,21 +293,21 @@ Example: "value" : [ { "value" : "636f6e74726163742063616c6c", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : "7472616e73666572", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : [ { "value" : "769162241eedf97c2481652adf1ba0f5bf57431b", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : "316e851039019d39dfc2c37d6c3fee19fd580987", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : "1000", @@ -351,20 +351,20 @@ Example: "value" : [ { "value" : "636f6e74726163742063616c6c", - "type" : "ByteArray" + "type" : "ByteString" }, { - "type" : "ByteArray", + "type" : "ByteString", "value" : "7472616e73666572" }, { "value" : [ { "value" : "769162241eedf97c2481652adf1ba0f5bf57431b", - "type" : "ByteArray" + "type" : "ByteString" }, { - "type" : "ByteArray", + "type" : "ByteString", "value" : "316e851039019d39dfc2c37d6c3fee19fd580987" }, { @@ -384,15 +384,15 @@ Example: "value" : [ { "value" : "7472616e73666572", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : "769162241eedf97c2481652adf1ba0f5bf57431b", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : "316e851039019d39dfc2c37d6c3fee19fd580987", - "type" : "ByteArray" + "type" : "ByteString" }, { "value" : "1000", diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 95bb7677b..d52e03221 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -206,7 +206,7 @@ func (c *codegen) scTypeFromExpr(typ ast.Expr) string { return "Struct" case *types.Slice: if isByte(t.Elem()) { - return "ByteArray" + return "ByteString" } return "Array" default: diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go index 0a7f8cbb8..6c5a415cd 100644 --- a/pkg/compiler/debug_test.go +++ b/pkg/compiler/debug_test.go @@ -63,7 +63,7 @@ func unexportedMethod() int { return 1 } returnTypes := map[string]string{ "MethodInt": "Integer", "MethodConcat": "String", - "MethodString": "String", "MethodByteArray": "ByteArray", + "MethodString": "String", "MethodByteArray": "ByteString", "MethodArray": "Array", "MethodStruct": "Struct", "Main": "Boolean", "unexportedMethod": "Integer", @@ -272,7 +272,7 @@ func TestDebugInfo_MarshalJSON(t *testing.T) { {"param1", "Integer"}, {"ok", "Boolean"}, }, - ReturnType: "ByteArray", + ReturnType: "ByteString", Variables: []string{}, SeqPoints: []DebugSeqPoint{ { diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 68d849882..cd85cb01d 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -591,7 +591,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { Trigger: trigger.System, VMState: v.State(), GasConsumed: v.GasConsumed(), - Stack: v.Estack().ToContractParameters(), + Stack: v.Estack().ToArray(), Events: systemInterop.Notifications, } appExecResults = append(appExecResults, aer) @@ -632,7 +632,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { Trigger: trigger.Application, VMState: v.State(), GasConsumed: v.GasConsumed(), - Stack: v.Estack().ToContractParameters(), + Stack: v.Estack().ToArray(), Events: systemInterop.Notifications, } appExecResults = append(appExecResults, aer) diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index a94649467..a9760b3dc 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -11,8 +11,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -103,7 +103,7 @@ func TestPutGetAppExecResult(t *testing.T) { appExecResult := &state.AppExecResult{ TxHash: hash, Events: []state.NotificationEvent{}, - Stack: []smartcontract.Parameter{}, + Stack: []stackitem.Item{}, } err := dao.PutAppExecResult(appExecResult) require.NoError(t, err) diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index b1df21ea1..df6bad3ed 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -131,8 +131,7 @@ func TestNativeContract_Invoke(t *testing.T) { require.NoError(t, err) require.Equal(t, vm.HaltState, res.VMState) require.Equal(t, 1, len(res.Stack)) - require.Equal(t, smartcontract.IntegerType, res.Stack[0].Type) - require.EqualValues(t, 42, res.Stack[0].Value) + require.Equal(t, big.NewInt(42), res.Stack[0].Value()) res, err = chain.GetAppExecResult(tx2.Hash()) require.NoError(t, err) diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index b0fd12964..88b246335 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -10,10 +10,10 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -29,20 +29,14 @@ func TestMaxTransactionsPerBlock(t *testing.T) { t.Run("get, contract method", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "getMaxTransactionsPerBlock") require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.IntegerType, - Value: 512, - }) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(512))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao) require.Equal(t, 1024, int(n)) @@ -56,27 +50,18 @@ func TestMaxBlockSize(t *testing.T) { t.Run("get", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "getMaxBlockSize") require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.IntegerType, - Value: 1024 * 256, - }) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1024*256))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) res, err = invokeNativePolicyMethod(chain, "getMaxBlockSize") require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.IntegerType, - Value: 102400, - }) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(102400))) require.NoError(t, chain.persist()) }) } @@ -93,20 +78,14 @@ func TestFeePerByte(t *testing.T) { t.Run("get, contract method", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "getFeePerByte") require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.IntegerType, - Value: 1000, - }) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1000))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) require.Equal(t, 1024, int(n)) @@ -127,20 +106,14 @@ func TestBlockedAccounts(t *testing.T) { t.Run("get, contract method", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "getBlockedAccounts") require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.ArrayType, - Value: []smartcontract.Parameter{}, - }) + checkResult(t, res, stackitem.NewArray([]stackitem.Item{})) require.NoError(t, chain.persist()) }) t.Run("block-unblock account", func(t *testing.T) { res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) accounts, err := chain.contracts.Policy.GetBlockedAccountsInternal(chain.dao) require.NoError(t, err) @@ -148,10 +121,7 @@ func TestBlockedAccounts(t *testing.T) { require.NoError(t, chain.persist()) res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) accounts, err = chain.contracts.Policy.GetBlockedAccountsInternal(chain.dao) require.NoError(t, err) @@ -163,35 +133,23 @@ func TestBlockedAccounts(t *testing.T) { // block res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) // double-block should fail res, err = invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: false, - }) + checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) // unblock res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) // unblock the same account should fail as we don't have it blocked res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: false, - }) + checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) }) @@ -205,10 +163,7 @@ func TestBlockedAccounts(t *testing.T) { for _, acc := range accounts { res, err := invokeNativePolicyMethod(chain, "blockAccount", acc.BytesBE()) require.NoError(t, err) - checkResult(t, res, smartcontract.Parameter{ - Type: smartcontract.BoolType, - Value: true, - }) + checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) } @@ -253,9 +208,8 @@ func invokeNativePolicyMethod(chain *Blockchain, method string, args ...interfac return res, nil } -func checkResult(t *testing.T, result *state.AppExecResult, expected smartcontract.Parameter) { +func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { require.Equal(t, vm.HaltState, result.VMState) require.Equal(t, 1, len(result.Stack)) - require.Equal(t, expected.Type, result.Stack[0].Type) - require.EqualValues(t, expected.Value, result.Stack[0].Value) + require.Equal(t, expected, result.Stack[0]) } diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 1818f4c7b..48027137d 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "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" @@ -26,7 +25,7 @@ type AppExecResult struct { Trigger trigger.Type VMState vm.State GasConsumed int64 - Stack []smartcontract.Parameter + Stack []stackitem.Item Events []NotificationEvent } @@ -59,7 +58,7 @@ func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { w.WriteB(byte(aer.Trigger)) w.WriteB(byte(aer.VMState)) w.WriteU64LE(uint64(aer.GasConsumed)) - w.WriteArray(aer.Stack) + stackitem.EncodeBinaryStackItem(stackitem.NewArray(aer.Stack), w) w.WriteArray(aer.Events) } @@ -69,6 +68,14 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { aer.Trigger = trigger.Type(r.ReadB()) aer.VMState = vm.State(r.ReadB()) aer.GasConsumed = int64(r.ReadU64LE()) - r.ReadArray(&aer.Stack) + item := stackitem.DecodeBinaryStackItem(r) + if r.Err == nil { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + r.Err = errors.New("array expected") + return + } + aer.Stack = arr + } r.ReadArray(&aer.Events) } diff --git a/pkg/core/state/notification_event_test.go b/pkg/core/state/notification_event_test.go index 5242b8544..7a24315ce 100644 --- a/pkg/core/state/notification_event_test.go +++ b/pkg/core/state/notification_event_test.go @@ -5,7 +5,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -26,7 +25,7 @@ func TestEncodeDecodeAppExecResult(t *testing.T) { Trigger: 1, VMState: vm.HaltState, GasConsumed: 10, - Stack: []smartcontract.Parameter{}, + Stack: []stackitem.Item{}, Events: []NotificationEvent{}, } diff --git a/pkg/rpc/client/nep5.go b/pkg/rpc/client/nep5.go index 150b44d4f..5f1618d9e 100644 --- a/pkg/rpc/client/nep5.go +++ b/pkg/rpc/client/nep5.go @@ -6,12 +6,12 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -192,46 +192,20 @@ func (c *Client) MultiTransferNEP5(acc *wallet.Account, token util.Uint160, gas return c.SendRawTransaction(tx) } -func topIntFromStack(st []smartcontract.Parameter) (int64, error) { +func topIntFromStack(st []stackitem.Item) (int64, error) { index := len(st) - 1 // top stack element is last in the array - var decimals int64 - switch typ := st[index].Type; typ { - case smartcontract.IntegerType: - var ok bool - decimals, ok = st[index].Value.(int64) - if !ok { - return 0, errors.New("invalid Integer item") - } - case smartcontract.ByteArrayType: - data, ok := st[index].Value.([]byte) - if !ok { - return 0, errors.New("invalid ByteArray item") - } - decimals = bigint.FromBytes(data).Int64() - default: - return 0, fmt.Errorf("invalid stack item type: %s", typ) + bi, err := st[index].TryInteger() + if err != nil { + return 0, err } - return decimals, nil + return bi.Int64(), nil } -func topStringFromStack(st []smartcontract.Parameter) (string, error) { +func topStringFromStack(st []stackitem.Item) (string, error) { index := len(st) - 1 // top stack element is last in the array - var s string - switch typ := st[index].Type; typ { - case smartcontract.StringType: - var ok bool - s, ok = st[index].Value.(string) - if !ok { - return "", errors.New("invalid String item") - } - case smartcontract.ByteArrayType: - data, ok := st[index].Value.([]byte) - if !ok { - return "", errors.New("invalid ByteArray item") - } - s = string(data) - default: - return "", fmt.Errorf("invalid stack item type: %s", typ) + bs, err := st[index].TryBytes() + if err != nil { + return "", err } - return s, nil + return string(bs), nil } diff --git a/pkg/rpc/client/policy.go b/pkg/rpc/client/policy.go index 4f9b15103..558491df5 100644 --- a/pkg/rpc/client/policy.go +++ b/pkg/rpc/client/policy.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/pkg/errors" ) @@ -51,27 +52,26 @@ func (c *Client) GetBlockedAccounts() (native.BlockedAccounts, error) { return topBlockedAccountsFromStack(result.Stack) } -func topBlockedAccountsFromStack(st []smartcontract.Parameter) (native.BlockedAccounts, error) { +func topBlockedAccountsFromStack(st []stackitem.Item) (native.BlockedAccounts, error) { index := len(st) - 1 // top stack element is last in the array var ( ba native.BlockedAccounts err error ) - switch typ := st[index].Type; typ { - case smartcontract.ArrayType: - data, ok := st[index].Value.([]smartcontract.Parameter) + items, ok := st[index].Value().([]stackitem.Item) + if !ok { + return nil, fmt.Errorf("invalid stack item type: %s", st[index].Type()) + } + ba = make(native.BlockedAccounts, len(items)) + for i, account := range items { + val, ok := account.Value().([]byte) if !ok { - return nil, errors.New("invalid Array item") + return nil, fmt.Errorf("invalid array element: %s", account.Type()) } - ba = make(native.BlockedAccounts, len(data)) - for i, account := range data { - ba[i], err = util.Uint160DecodeBytesLE(account.Value.([]byte)) - if err != nil { - return nil, err - } + ba[i], err = util.Uint160DecodeBytesLE(val) + if err != nil { + return nil, err } - default: - return nil, fmt.Errorf("invalid stack item type: %s", typ) } return ba, nil } diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 20e35d979..07b683fd7 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -27,6 +27,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -638,7 +639,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ Account: util.Uint160{1, 2, 3}, }}) }, - serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf","state":"HALT","gasconsumed":"31100000","stack":[{"type":"ByteArray","value":"JivsCEQy"}],"tx":"d101361426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf000000000000000000000000"}}`, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf","state":"HALT","gasconsumed":"31100000","stack":[{"type":"ByteString","value":"JivsCEQy"}],"tx":"d101361426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf000000000000000000000000"}}`, result: func(c *Client) interface{} { bytes, err := hex.DecodeString("262bec084432") if err != nil { @@ -648,12 +649,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ State: "HALT", GasConsumed: 31100000, Script: "1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf", - Stack: []smartcontract.Parameter{ - { - Type: smartcontract.ByteArrayType, - Value: bytes, - }, - }, + Stack: []stackitem.Item{stackitem.NewByteArray(bytes)}, } }, }, @@ -670,7 +666,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ Account: util.Uint160{1, 2, 3}, }}) }, - serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"00046e616d656724058e5e1b6008847cd662728549088a9ee82191","state":"HALT","gasconsumed":"16100000","stack":[{"type":"ByteArray","value":"TkVQNSBHQVM="}],"tx":"d1011b00046e616d656724058e5e1b6008847cd662728549088a9ee82191000000000000000000000000"}}`, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"00046e616d656724058e5e1b6008847cd662728549088a9ee82191","state":"HALT","gasconsumed":"16100000","stack":[{"type":"ByteString","value":"TkVQNSBHQVM="}],"tx":"d1011b00046e616d656724058e5e1b6008847cd662728549088a9ee82191000000000000000000000000"}}`, result: func(c *Client) interface{} { bytes, err := hex.DecodeString("4e45503520474153") if err != nil { @@ -680,12 +676,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ State: "HALT", GasConsumed: 16100000, Script: "00046e616d656724058e5e1b6008847cd662728549088a9ee82191", - Stack: []smartcontract.Parameter{ - { - Type: smartcontract.ByteArrayType, - Value: bytes, - }, - }, + Stack: []stackitem.Item{stackitem.NewByteArray(bytes)}, } }, }, diff --git a/pkg/rpc/client/wsclient_test.go b/pkg/rpc/client/wsclient_test.go index f48ce3d60..79e0af19d 100644 --- a/pkg/rpc/client/wsclient_test.go +++ b/pkg/rpc/client/wsclient_test.go @@ -116,9 +116,9 @@ func TestWSClientEvents(t *testing.T) { var ok bool // Events from RPC server test chain. var events = []string{ - `{"jsonrpc":"2.0","method":"transaction_executed","params":[{"txid":"0xe1cd5e57e721d2a2e05fb1f08721b12057b25ab1dd7fd0f33ee1639932fdfad7","trigger":"Application","vmstate":"HALT","gasconsumed":"22910000","stack":[],"notifications":[{"contract":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","state":{"type":"Array","value":[{"type":"ByteArray","value":"Y29udHJhY3QgY2FsbA=="},{"type":"ByteArray","value":"dHJhbnNmZXI="},{"type":"Array","value":[{"type":"ByteArray","value":"dpFiJB7t+XwkgWUq3xug9b9XQxs="},{"type":"ByteArray","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"Integer","value":"1000"}]}]}},{"contract":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","state":{"type":"Array","value":[{"type":"ByteArray","value":"dHJhbnNmZXI="},{"type":"ByteArray","value":"dpFiJB7t+XwkgWUq3xug9b9XQxs="},{"type":"ByteArray","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"Integer","value":"1000"}]}}]}]}`, - `{"jsonrpc":"2.0","method":"notification_from_execution","params":[{"contract":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","state":{"type":"Array","value":[{"type":"ByteArray","value":"Y29udHJhY3QgY2FsbA=="},{"type":"ByteArray","value":"dHJhbnNmZXI="},{"type":"Array","value":[{"type":"ByteArray","value":"dpFiJB7t+XwkgWUq3xug9b9XQxs="},{"type":"ByteArray","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"Integer","value":"1000"}]}]}}]}`, - `{"jsonrpc":"2.0","method":"transaction_executed","params":[{"txid":"0xf97a72b7722c109f909a8bc16c22368c5023d85828b09b127b237aace33cf099","trigger":"Application","vmstate":"HALT","gasconsumed":"6042610","stack":[],"notifications":[{"contract":"0xe65ff7b3a02d207b584a5c27057d4e9862ef01da","state":{"type":"Array","value":[{"type":"ByteArray","value":"Y29udHJhY3QgY2FsbA=="},{"type":"ByteArray","value":"dHJhbnNmZXI="},{"type":"Array","value":[{"type":"ByteArray","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"ByteArray","value":"IHKCdK+vw29DoHHTKM+j5inZy7A="},{"type":"Integer","value":"123"}]}]}},{"contract":"0xe65ff7b3a02d207b584a5c27057d4e9862ef01da","state":{"type":"Array","value":[{"type":"ByteArray","value":"dHJhbnNmZXI="},{"type":"ByteArray","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"ByteArray","value":"IHKCdK+vw29DoHHTKM+j5inZy7A="},{"type":"Integer","value":"123"}]}}]}]}`, + `{"jsonrpc":"2.0","method":"transaction_executed","params":[{"txid":"0xe1cd5e57e721d2a2e05fb1f08721b12057b25ab1dd7fd0f33ee1639932fdfad7","trigger":"Application","vmstate":"HALT","gasconsumed":"22910000","stack":[],"notifications":[{"contract":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","eventname":"contract call","state":{"type":"Array","value":[{"type":"ByteString","value":"dHJhbnNmZXI="},{"type":"Array","value":[{"type":"ByteString","value":"dpFiJB7t+XwkgWUq3xug9b9XQxs="},{"type":"ByteString","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"Integer","value":"1000"}]}]}},{"contract":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","eventname":"transfer","state":{"type":"Array","value":[{"type":"ByteString","value":"dpFiJB7t+XwkgWUq3xug9b9XQxs="},{"type":"ByteString","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"Integer","value":"1000"}]}}]}]}`, + `{"jsonrpc":"2.0","method":"notification_from_execution","params":[{"contract":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","eventname":"contract call","state":{"type":"Array","value":[{"type":"ByteString","value":"dHJhbnNmZXI="},{"type":"Array","value":[{"type":"ByteString","value":"dpFiJB7t+XwkgWUq3xug9b9XQxs="},{"type":"ByteString","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"Integer","value":"1000"}]}]}}]}`, + `{"jsonrpc":"2.0","method":"transaction_executed","params":[{"txid":"0xf97a72b7722c109f909a8bc16c22368c5023d85828b09b127b237aace33cf099","trigger":"Application","vmstate":"HALT","gasconsumed":"6042610","stack":[],"notifications":[{"contract":"0xe65ff7b3a02d207b584a5c27057d4e9862ef01da","eventname":"contract call","state":{"type":"Array","value":[{"type":"ByteString","value":"dHJhbnNmZXI="},{"type":"Array","value":[{"type":"ByteString","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"ByteString","value":"IHKCdK+vw29DoHHTKM+j5inZy7A="},{"type":"Integer","value":"123"}]}]}},{"contract":"0xe65ff7b3a02d207b584a5c27057d4e9862ef01da","eventname":"transfer","state":{"type":"Array","value":[{"type":"ByteString","value":"MW6FEDkBnTnfwsN9bD/uGf1YCYc="},{"type":"ByteString","value":"IHKCdK+vw29DoHHTKM+j5inZy7A="},{"type":"Integer","value":"123"}]}}]}]}`, `{"jsonrpc":"2.0","method":"block_added","params":[{"hash":"0x2d312f6379ead13cf62634c703091b750e7903728df2a3cf5bd96ce80b84a849","version":0,"previousblockhash":"0xb8237d34c156cac6be7b01578decf8ac8c99a62f0b6f774d622aad7be0fe189d","merkleroot":"0xf89169e89361692b71e671f13c088e84c5325015c413e8f89e7ba38efdb41287","time":1592472500006,"index":6,"nextconsensus":"Nbb1qkwcwNSBs9pAnrVVrnFbWnbWBk91U2","witnesses":[{"invocation":"DEDblVguNGXWbUswDvBfVJzBt76BJyJ0Ga6siquyjioGn4Dbr6zy1IvcLl3xN5akcejRy9e+Mr1qvpe/gkLgtW4QDEDRwPISZagMFjE/plXTnZ/gEU0IbBAAe23U29zVWteUmzRsPxF/MdzXvdffR9W0edkj17AmkWpn+5rqzH9aCOpLDECEvjgxZaRoAHEDNzp1REllLcGzMwrwSjudtzfgRglQL3g1BKerDx6cGHH73medRVkL9QVm4KzSxlywVtvhwBMrDEBuPKvzg5TtakFW2jr/bfmy1bn2FiLARlOySwaGdKRV93ozA5lVEIAvHbBlJtT4/5H8jHjbncXXMrP3OUHqebZz","verification":"EwwhAhA6f33QFlWFl/eWDSfFFqQ5T9loueZRVetLAT5AQEBuDCECp7xV/oaE4BGXaNEEujB5W9zIZhnoZK3SYVZyPtGFzWIMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwgwhA9kMB99j5pDOd5EuEKtRrMlEtmhgI3tgjE+PgwnnHuaZFAtBMHOzuw=="}],"consensusdata":{"primary":0,"nonce":"0000000000000457"},"tx":[{"hash":"0xf97a72b7722c109f909a8bc16c22368c5023d85828b09b127b237aace33cf099","size":265,"version":0,"nonce":9,"sender":"NQRLhCpAru9BjGsMwk67vdMwmzKMRgsnnN","sysfee":"0","netfee":"365210","validuntilblock":1200,"attributes":[],"cosigners":[{"account":"0x870958fd19ee3f6c7dc3c2df399d013910856e31","scopes":"CalledByEntry"}],"script":"AHsMFCBygnSvr8NvQ6Bx0yjPo+Yp2cuwDBQxboUQOQGdOd/Cw31sP+4Z/VgJhxPADAh0cmFuc2ZlcgwU2gHvYphOfQUnXEpYeyAtoLP3X+ZBYn1bUjg=","witnesses":[{"invocation":"DECwklSj3liZOJbktRtkVdUCu8U2LQlrU6Dv8NtMgd0xXbk5lXjc2p68xv6xtJXbJ4aoFMJZ9lkcNpGoeUCcaCet","verification":"DCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcILQQqQatQ="}]}]}]}`, `{"jsonrpc":"2.0","method":"event_missed","params":[]}`, } diff --git a/pkg/rpc/response/result/application_log.go b/pkg/rpc/response/result/application_log.go index 8d93e005b..9ecae405f 100644 --- a/pkg/rpc/response/result/application_log.go +++ b/pkg/rpc/response/result/application_log.go @@ -21,6 +21,7 @@ type ApplicationLog struct { //NotificationEvent response wrapper type NotificationEvent struct { Contract util.Uint160 `json:"contract"` + Name string `json:"eventname"` Item smartcontract.Parameter `json:"state"` } @@ -28,13 +29,10 @@ type NotificationEvent struct { // result.NotificationEvent. func StateEventToResultNotification(event state.NotificationEvent) NotificationEvent { seen := make(map[stackitem.Item]bool) - args := stackitem.NewArray([]stackitem.Item{ - stackitem.Make(event.Name), - event.Item, - }) - item := smartcontract.ParameterFromStackItem(args, seen) + item := smartcontract.ParameterFromStackItem(event.Item, seen) return NotificationEvent{ Contract: event.ScriptHash, + Name: event.Name, Item: item, } } @@ -45,13 +43,17 @@ func NewApplicationLog(appExecRes *state.AppExecResult) ApplicationLog { for _, e := range appExecRes.Events { events = append(events, StateEventToResultNotification(e)) } - + st := make([]smartcontract.Parameter, len(appExecRes.Stack)) + seen := make(map[stackitem.Item]bool) + for i := range appExecRes.Stack { + st[i] = smartcontract.ParameterFromStackItem(appExecRes.Stack[i], seen) + } return ApplicationLog{ TxHash: appExecRes.TxHash, Trigger: appExecRes.Trigger.String(), VMState: appExecRes.VMState.String(), GasConsumed: appExecRes.GasConsumed, - Stack: appExecRes.Stack, + Stack: st, Events: events, } } diff --git a/pkg/rpc/response/result/invoke.go b/pkg/rpc/response/result/invoke.go index 16c877275..bab39a104 100644 --- a/pkg/rpc/response/result/invoke.go +++ b/pkg/rpc/response/result/invoke.go @@ -1,14 +1,76 @@ package result import ( - "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "encoding/json" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) // Invoke represents code invocation result and is used by several RPC calls // that invoke functions, scripts and generic bytecode. type Invoke struct { - State string `json:"state"` - GasConsumed int64 `json:"gasconsumed,string"` - Script string `json:"script"` - Stack []smartcontract.Parameter `json:"stack"` + State string `json:"state"` + GasConsumed int64 `json:"gasconsumed,string"` + Script string `json:"script"` + Stack []stackitem.Item `json:"stack"` +} + +type invokeAux struct { + State string `json:"state"` + GasConsumed int64 `json:"gasconsumed,string"` + Script string `json:"script"` + Stack json.RawMessage `json:"stack"` +} + +// MarshalJSON implements json.Marshaler. +func (r Invoke) MarshalJSON() ([]byte, error) { + var st json.RawMessage + arr := make([]json.RawMessage, len(r.Stack)) + for i := range arr { + data, err := stackitem.ToJSONWithTypes(r.Stack[i]) + if err != nil { + st = []byte("error: recursive reference") + break + } + arr[i] = data + } + + var err error + if st == nil { + st, err = json.Marshal(arr) + if err != nil { + return nil, err + } + } + return json.Marshal(&invokeAux{ + GasConsumed: r.GasConsumed, + Script: r.Script, + State: r.State, + Stack: st, + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (r *Invoke) UnmarshalJSON(data []byte) error { + aux := new(invokeAux) + if err := json.Unmarshal(data, aux); err != nil { + return err + } + var arr []json.RawMessage + if err := json.Unmarshal(aux.Stack, &arr); err == nil { + st := make([]stackitem.Item, len(arr)) + for i := range arr { + st[i], err = stackitem.FromJSONWithTypes(arr[i]) + if err != nil { + break + } + } + if err == nil { + r.Stack = st + } + } + r.GasConsumed = aux.GasConsumed + r.Script = aux.Script + r.State = aux.State + return nil } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 5e661ac71..47873fdc7 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -22,7 +22,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpc" @@ -623,14 +622,11 @@ func (s *Server) getDecimals(contractID int32, cache map[int32]decimals) (decima } d := decimals{Hash: h} - switch item := res.Stack[len(res.Stack)-1]; item.Type { - case smartcontract.IntegerType: - d.Value = item.Value.(int64) - case smartcontract.ByteArrayType: - d.Value = bigint.FromBytes(item.Value.([]byte)).Int64() - default: - return d, errors.New("invalid result: not an integer") + bi, err := res.Stack[len(res.Stack)-1].TryInteger() + if err != nil { + return decimals{}, err } + d.Value = bi.Int64() if d.Value < 0 { return d, errors.New("incorrect result: negative result") } @@ -896,7 +892,7 @@ func (s *Server) runScriptInVM(script []byte, tx *transaction.Transaction) *resu State: vm.State().String(), GasConsumed: vm.GasConsumed(), Script: hex.EncodeToString(script), - Stack: vm.Estack().ToContractParameters(), + Stack: vm.Estack().ToArray(), } return result } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index a9a0d37bc..acc15d1f4 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -531,7 +531,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) assert.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) - require.Equal(t, int64(3), res.Stack[0].Value) + require.Equal(t, big.NewInt(3), res.Stack[0].Value()) }, }, { @@ -543,7 +543,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) assert.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) - require.Equal(t, int64(2), res.Stack[0].Value) + require.Equal(t, big.NewInt(2), res.Stack[0].Value()) }, }, { @@ -555,7 +555,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) assert.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) - require.Equal(t, int64(1), res.Stack[0].Value) + require.Equal(t, big.NewInt(1), res.Stack[0].Value()) }, }, { @@ -567,7 +567,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) assert.Equal(t, "HALT", res.State) assert.Equal(t, 1, len(res.Stack)) - assert.Equal(t, int64(1), res.Stack[0].Value) + assert.Equal(t, big.NewInt(1), res.Stack[0].Value()) }, }, { diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index 818181834..8a9481f9e 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -32,11 +32,11 @@ var marshalJSONTestCases = []struct { }, { input: Parameter{Type: ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, - result: `{"type":"ByteArray","value":"` + hexToBase64("010203") + `"}`, + result: `{"type":"ByteString","value":"` + hexToBase64("010203") + `"}`, }, { input: Parameter{Type: ByteArrayType}, - result: `{"type":"ByteArray","value":null}`, + result: `{"type":"ByteString","value":null}`, }, { input: Parameter{ @@ -68,8 +68,8 @@ var marshalJSONTestCases = []struct { }}, }, }, - result: `{"type":"Array","value":[{"type":"ByteArray","value":"` + hexToBase64("0102") + `"},{"type":"Array","value":[` + - `{"type":"ByteArray","value":"` + hexToBase64("030201") + `"},{"type":"ByteArray","value":"` + hexToBase64("070809") + `"}]}]}`, + result: `{"type":"Array","value":[{"type":"ByteString","value":"` + hexToBase64("0102") + `"},{"type":"Array","value":[` + + `{"type":"ByteString","value":"` + hexToBase64("030201") + `"},{"type":"ByteString","value":"` + hexToBase64("070809") + `"}]}]}`, }, { input: Parameter{ @@ -185,7 +185,7 @@ var unmarshalJSONTestCases = []struct { result: Parameter{Type: IntegerType, Value: int64(12345)}, }, { - input: `{"type":"ByteArray","value":"` + hexToBase64("010203") + `"}`, + input: `{"type":"ByteString","value":"` + hexToBase64("010203") + `"}`, result: Parameter{Type: ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, }, { @@ -288,9 +288,9 @@ var unmarshalJSONTestCases = []struct { } var unmarshalJSONErrorCases = []string{ - `{"type": "ByteArray","value":`, // incorrect JSON - `{"type": "ByteArray","value":1}`, // incorrect Value - `{"type": "ByteArray","value":"12^"}`, // incorrect ByteArray value + `{"type": "ByteString","value":`, // incorrect JSON + `{"type": "ByteString","value":1}`, // incorrect Value + `{"type": "ByteString","value":"12^"}`, // incorrect ByteArray value `{"type": "String","value":`, // incorrect JSON `{"type": "String","value":1}`, // incorrect Value `{"type": "Integer","value": "nn"}`, // incorrect Integer value diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index a66058d3f..969a65a1b 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -6,7 +6,6 @@ import ( "fmt" "math/big" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -406,18 +405,24 @@ func (s *Stack) PopSigElements() ([][]byte, error) { return elems, nil } -// ToContractParameters converts Stack to slice of smartcontract.Parameter. -func (s *Stack) ToContractParameters() []smartcontract.Parameter { - items := make([]smartcontract.Parameter, 0, s.Len()) +// ToArray converts stack to an array of stackitems with top item being the last. +func (s *Stack) ToArray() []stackitem.Item { + items := make([]stackitem.Item, 0, s.len) s.IterBack(func(e *Element) { - // Each item is independent. - seen := make(map[stackitem.Item]bool) - items = append(items, smartcontract.ParameterFromStackItem(e.value, seen)) + items = append(items, e.Item()) }) return items } // MarshalJSON implements JSON marshalling interface. func (s *Stack) MarshalJSON() ([]byte, error) { - return json.Marshal(s.ToContractParameters()) + items := s.ToArray() + arr := make([]json.RawMessage, len(items)) + for i := range items { + data, err := stackitem.ToJSONWithTypes(items[i]) + if err == nil { + arr[i] = data + } + } + return json.Marshal(arr) } diff --git a/pkg/vm/stack_test.go b/pkg/vm/stack_test.go index 82c15e962..885ccbf08 100644 --- a/pkg/vm/stack_test.go +++ b/pkg/vm/stack_test.go @@ -237,6 +237,22 @@ func TestPushVal(t *testing.T) { assert.IsType(t, elem.value, &stackitem.Array{}) } +func TestStack_ToArray(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + s := NewStack("test") + items := s.ToArray() + require.Equal(t, 0, len(items)) + }) + t.Run("NonEmpty", func(t *testing.T) { + s := NewStack("test") + expected := []stackitem.Item{stackitem.Make(1), stackitem.Make(true)} + for i := range expected { + s.PushVal(expected[i]) + } + require.Equal(t, expected, s.ToArray()) + }) +} + func TestSwapElemValues(t *testing.T) { s := NewStack("test") diff --git a/pkg/vm/stackitem/item.go b/pkg/vm/stackitem/item.go index 4cd138e93..63c407948 100644 --- a/pkg/vm/stackitem/item.go +++ b/pkg/vm/stackitem/item.go @@ -215,7 +215,7 @@ func (i *Struct) Bool() bool { return true } // TryBytes implements Item interface. func (i *Struct) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Struct to ByteArray") + return nil, errors.New("can't convert Struct to ByteString") } // TryInteger implements Item interface. @@ -301,7 +301,7 @@ func (i Null) Bool() bool { return false } // TryBytes implements Item interface. func (i Null) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Null to ByteArray") + return nil, errors.New("can't convert Null to ByteString") } // TryInteger implements Item interface. @@ -497,7 +497,7 @@ func (i *ByteArray) MarshalJSON() ([]byte, error) { } func (i *ByteArray) String() string { - return "ByteArray" + return "ByteString" } // Bool implements Item interface. @@ -606,7 +606,7 @@ func (i *Array) Bool() bool { return true } // TryBytes implements Item interface. func (i *Array) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Array to ByteArray") + return nil, errors.New("can't convert Array to ByteString") } // TryInteger implements Item interface. @@ -696,7 +696,7 @@ func (i *Map) Bool() bool { return true } // TryBytes implements Item interface. func (i *Map) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Map to ByteArray") + return nil, errors.New("can't convert Map to ByteString") } // TryInteger implements Item interface. @@ -812,7 +812,7 @@ func (i *Interop) Bool() bool { return true } // TryBytes implements Item interface. func (i *Interop) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Interop to ByteArray") + return nil, errors.New("can't convert Interop to ByteString") } // TryInteger implements Item interface. @@ -893,7 +893,7 @@ func (p *Pointer) Bool() bool { // TryBytes implements Item interface. func (p *Pointer) TryBytes() ([]byte, error) { - return nil, errors.New("can't convert Pointer to ByteArray") + return nil, errors.New("can't convert Pointer to ByteString") } // TryInteger implements Item interface. diff --git a/pkg/vm/stackitem/item_test.go b/pkg/vm/stackitem/item_test.go index 978024f49..723d0a489 100644 --- a/pkg/vm/stackitem/item_test.go +++ b/pkg/vm/stackitem/item_test.go @@ -112,7 +112,7 @@ var stringerTestCases = []struct { }, { input: NewByteArray([]byte{}), - result: "ByteArray", + result: "ByteString", }, { input: NewArray([]Item{}), diff --git a/pkg/vm/stackitem/json.go b/pkg/vm/stackitem/json.go index 97d25f883..4a4b359a7 100644 --- a/pkg/vm/stackitem/json.go +++ b/pkg/vm/stackitem/json.go @@ -208,3 +208,171 @@ func (d *decoder) decodeMap() (*Map, error) { m.Add(NewByteArray([]byte(k)), val) } } + +// ToJSONWithTypes serializes any stackitem to JSON in a lossless way. +func ToJSONWithTypes(item Item) ([]byte, error) { + result, err := toJSONWithTypes(item, make(map[Item]bool)) + if err != nil { + return nil, err + } + return json.Marshal(result) +} + +func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) { + typ := item.Type() + result := map[string]interface{}{ + "type": typ.String(), + } + var value interface{} + switch it := item.(type) { + case *Array, *Struct: + if seen[item] { + return "", errors.New("recursive structures can't be serialized to json") + } + seen[item] = true + arr := []interface{}{} + for _, elem := range it.Value().([]Item) { + s, err := toJSONWithTypes(elem, seen) + if err != nil { + return "", err + } + arr = append(arr, s) + } + value = arr + case *Bool: + value = it.value + case *Buffer, *ByteArray: + value = base64.StdEncoding.EncodeToString(it.Value().([]byte)) + case *BigInteger: + value = it.value.String() + case *Map: + if seen[item] { + return "", errors.New("recursive structures can't be serialized to json") + } + seen[item] = true + arr := []interface{}{} + for i := range it.value { + // map keys are primitive types and can always be converted to json + key, _ := toJSONWithTypes(it.value[i].Key, seen) + val, err := toJSONWithTypes(it.value[i].Value, seen) + if err != nil { + return "", err + } + arr = append(arr, map[string]interface{}{ + "key": key, + "value": val, + }) + } + value = arr + case *Pointer: + value = it.pos + } + if value != nil { + result["value"] = value + } + return result, nil +} + +type ( + rawItem struct { + Type string `json:"type"` + Value json.RawMessage `json:"value,omitempty"` + } + + rawMapElement struct { + Key json.RawMessage `json:"key"` + Value json.RawMessage `json:"value"` + } +) + +// FromJSONWithTypes deserializes an item from typed-json representation. +func FromJSONWithTypes(data []byte) (Item, error) { + raw := new(rawItem) + if err := json.Unmarshal(data, raw); err != nil { + return nil, err + } + typ, err := FromString(raw.Type) + if err != nil { + return nil, errors.New("invalid type") + } + switch typ { + case AnyT: + return Null{}, nil + case PointerT: + var pos int + if err := json.Unmarshal(raw.Value, &pos); err != nil { + return nil, err + } + return NewPointer(pos, nil), nil + case BooleanT: + var b bool + if err := json.Unmarshal(raw.Value, &b); err != nil { + return nil, err + } + return NewBool(b), nil + case IntegerT: + var s string + if err := json.Unmarshal(raw.Value, &s); err != nil { + return nil, err + } + val, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, errors.New("invalid integer") + } + return NewBigInteger(val), nil + case ByteArrayT, BufferT: + var s string + if err := json.Unmarshal(raw.Value, &s); err != nil { + return nil, err + } + val, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + if typ == ByteArrayT { + return NewByteArray(val), nil + } + return NewBuffer(val), nil + case ArrayT, StructT: + var arr []json.RawMessage + if err := json.Unmarshal(raw.Value, &arr); err != nil { + return nil, err + } + items := make([]Item, len(arr)) + for i := range arr { + it, err := FromJSONWithTypes(arr[i]) + if err != nil { + return nil, err + } + items[i] = it + } + if typ == ArrayT { + return NewArray(items), nil + } + return NewStruct(items), nil + case MapT: + var arr []rawMapElement + if err := json.Unmarshal(raw.Value, &arr); err != nil { + return nil, err + } + m := NewMap() + for i := range arr { + key, err := FromJSONWithTypes(arr[i].Key) + if err != nil { + return nil, err + } else if !IsValidMapKey(key) { + return nil, fmt.Errorf("invalid map key of type %s", key.Type()) + } + value, err := FromJSONWithTypes(arr[i].Value) + if err != nil { + return nil, err + } + m.Add(key, value) + } + return m, nil + case InteropT: + return NewInterop(nil), nil + default: + return nil, errors.New("unexpected type") + } +} diff --git a/pkg/vm/stackitem/json_test.go b/pkg/vm/stackitem/json_test.go index 1b1ef7718..bb9e17acd 100644 --- a/pkg/vm/stackitem/json_test.go +++ b/pkg/vm/stackitem/json_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -103,3 +104,137 @@ func TestFromToJSON(t *testing.T) { }) }) } + +// This test is taken from the C# code +// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/VM/UT_Helper.cs#L30 +func TestToJSONWithTypeCompat(t *testing.T) { + items := []Item{ + Make(5), Make("hello world"), + Make([]byte{1, 2, 3}), Make(true), + } + + // Note: we use `Equal` and not `JSONEq` because there are no spaces and maps so the order is well-defined. + s, err := ToJSONWithTypes(items[0]) + assert.NoError(t, err) + assert.Equal(t, `{"type":"Integer","value":"5"}`, string(s)) + + s, err = ToJSONWithTypes(items[1]) + assert.NoError(t, err) + assert.Equal(t, `{"type":"ByteString","value":"aGVsbG8gd29ybGQ="}`, string(s)) + + s, err = ToJSONWithTypes(items[2]) + assert.NoError(t, err) + assert.Equal(t, `{"type":"ByteString","value":"AQID"}`, string(s)) + + s, err = ToJSONWithTypes(items[3]) + assert.NoError(t, err) + assert.Equal(t, `{"type":"Boolean","value":true}`, string(s)) + + s, err = ToJSONWithTypes(NewArray(items)) + assert.NoError(t, err) + assert.Equal(t, `{"type":"Array","value":[{"type":"Integer","value":"5"},{"type":"ByteString","value":"aGVsbG8gd29ybGQ="},{"type":"ByteString","value":"AQID"},{"type":"Boolean","value":true}]}`, string(s)) + + item := NewMap() + item.Add(Make(1), NewPointer(0, []byte{0})) + s, err = ToJSONWithTypes(item) + assert.NoError(t, err) + assert.Equal(t, `{"type":"Map","value":[{"key":{"type":"Integer","value":"1"},"value":{"type":"Pointer","value":0}}]}`, string(s)) +} + +func TestToJSONWithTypes(t *testing.T) { + testCases := []struct { + name string + item Item + result string + }{ + {"Null", Null{}, `{"type":"Any"}`}, + {"Integer", NewBigInteger(big.NewInt(42)), `{"type":"Integer","value":"42"}`}, + {"ByteString", NewByteArray([]byte{1, 2, 3}), `{"type":"ByteString","value":"AQID"}`}, + {"Buffer", NewBuffer([]byte{1, 2, 3}), `{"type":"Buffer","value":"AQID"}`}, + {"BoolTrue", NewBool(true), `{"type":"Boolean","value":true}`}, + {"BoolFalse", NewBool(false), `{"type":"Boolean","value":false}`}, + {"Struct", NewStruct([]Item{Make(11)}), + `{"type":"Struct","value":[{"type":"Integer","value":"11"}]}`}, + {"Map", NewMapWithValue([]MapElement{{Key: NewBigInteger(big.NewInt(42)), Value: NewBool(false)}}), + `{"type":"Map","value":[{"key":{"type":"Integer","value":"42"},` + + `"value":{"type":"Boolean","value":false}}]}`}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s, err := ToJSONWithTypes(tc.item) + require.NoError(t, err) + require.Equal(t, tc.result, string(s)) + + item, err := FromJSONWithTypes(s) + require.NoError(t, err) + require.Equal(t, tc.item, item) + }) + } + + t.Run("Invalid", func(t *testing.T) { + t.Run("RecursiveArray", func(t *testing.T) { + arr := NewArray(nil) + arr.value = []Item{Make(5), arr, Make(true)} + + _, err := ToJSONWithTypes(arr) + require.Error(t, err) + }) + t.Run("RecursiveMap", func(t *testing.T) { + m := NewMap() + m.Add(Make(3), Make(true)) + m.Add(Make(5), m) + + _, err := ToJSONWithTypes(m) + require.Error(t, err) + }) + }) +} + +func TestFromJSONWithTypes(t *testing.T) { + testCases := []struct { + name string + json string + item Item + }{ + {"Pointer", `{"type":"Pointer","value":3}`, NewPointer(3, nil)}, + {"Interop", `{"type":"Interop"}`, NewInterop(nil)}, + {"Null", `{"type":"Any"}`, Null{}}, + {"Array", `{"type":"Array","value":[{"type":"Any"}]}`, NewArray([]Item{Null{}})}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + item, err := FromJSONWithTypes([]byte(tc.json)) + require.NoError(t, err) + require.Equal(t, tc.item, item) + }) + } + + t.Run("Invalid", func(t *testing.T) { + errCases := []struct { + name string + json string + }{ + {"InvalidType", `{"type":int,"value":"4"`}, + {"UnexpectedType", `{"type":"int","value":"4"}`}, + {"IntegerValue1", `{"type":"Integer","value": 4}`}, + {"IntegerValue2", `{"type":"Integer","value": "a"}`}, + {"BoolValue", `{"type":"Boolean","value": "str"}`}, + {"PointerValue", `{"type":"Pointer","value": "str"}`}, + {"BufferValue1", `{"type":"Buffer","value":"not a base 64"}`}, + {"BufferValue2", `{"type":"Buffer","value":123}`}, + {"ArrayValue", `{"type":"Array","value":3}`}, + {"ArrayElement", `{"type":"Array","value":[3]}`}, + {"MapValue", `{"type":"Map","value":3}`}, + {"MapElement", `{"type":"Map","value":[{"key":"value"}]}`}, + {"MapElementKeyNotPrimitive", `{"type":"Map","value":[{"key":{"type":"Any"}}]}`}, + {"MapElementValue", `{"type":"Map","value":[` + + `{"key":{"type":"Integer","value":"3"},"value":3}]}`}, + } + for _, tc := range errCases { + t.Run(tc.name, func(t *testing.T) { + _, err := FromJSONWithTypes([]byte(tc.json)) + require.Error(t, err) + }) + } + }) +} diff --git a/pkg/vm/stackitem/type.go b/pkg/vm/stackitem/type.go index 303e28508..ea69fc48b 100644 --- a/pkg/vm/stackitem/type.go +++ b/pkg/vm/stackitem/type.go @@ -1,5 +1,7 @@ package stackitem +import "errors" + // Type represents type of the stack item. type Type byte @@ -29,7 +31,7 @@ func (t Type) String() string { case IntegerT: return "Integer" case ByteArrayT: - return "ByteArray" + return "ByteString" case BufferT: return "Buffer" case ArrayT: @@ -54,3 +56,31 @@ func (t Type) IsValid() bool { return false } } + +// FromString returns stackitem type from string. +func FromString(s string) (Type, error) { + switch s { + case "Any": + return AnyT, nil + case "Pointer": + return PointerT, nil + case "Boolean": + return BooleanT, nil + case "Integer": + return IntegerT, nil + case "ByteString": + return ByteArrayT, nil + case "Buffer": + return BufferT, nil + case "Array": + return ArrayT, nil + case "Struct": + return StructT, nil + case "Map": + return MapT, nil + case "Interop": + return InteropT, nil + default: + return 0xFF, errors.New("invalid type") + } +} diff --git a/pkg/vm/stackitem/type_test.go b/pkg/vm/stackitem/type_test.go new file mode 100644 index 000000000..dd550fc7d --- /dev/null +++ b/pkg/vm/stackitem/type_test.go @@ -0,0 +1,16 @@ +package stackitem + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFromString(t *testing.T) { + typs := []Type{AnyT, PointerT, BooleanT, IntegerT, ByteArrayT, BufferT, ArrayT, StructT, MapT, InteropT} + for _, typ := range typs { + actual, err := FromString(typ.String()) + require.NoError(t, err) + require.Equal(t, typ, actual) + } +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index dcef4e0e5..a1cd02358 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -321,7 +321,7 @@ func (v *VM) Stack(n string) string { if n == "estack" { s = v.estack } - b, _ := json.MarshalIndent(s.ToContractParameters(), "", " ") + b, _ := json.MarshalIndent(s, "", " ") return string(b) }