From 648e0bb242da9daaeef4f1bbbad69367fc26c836 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 21 Feb 2020 17:34:18 +0300 Subject: [PATCH 1/5] rpc/smartcontract: merge contract parameter types Merged two types: - smartcontract.ParamType - rpc.StackParamType into single one: - smartcontract.ParamType as they duplicated the functionality. NOTE: type smartcontract.MapType was added (as in C# implementation). From now, list of supported smartcontract parameter types: UnknownType SignatureType BoolType IntegerType Hash160Type Hash256Type ByteArrayType PublicKeyType StringType ArrayType MapType InteropInterfaceType VoidType --- cli/smartcontract/smart_contract.go | 8 +- pkg/rpc/request/param.go | 5 +- pkg/rpc/request/param_test.go | 5 +- pkg/rpc/request/scdetails.go | 6 +- pkg/rpc/request/stack_param.go | 291 --------------- pkg/rpc/request/stack_param_test.go | 200 ----------- pkg/rpc/request/txBuilder.go | 14 +- pkg/rpc/request/tx_builder_test.go | 41 +-- pkg/rpc/response/types.go | 4 +- pkg/smartcontract/param_context.go | 382 +++++++++----------- pkg/smartcontract/param_context_test.go | 457 ++++++++++-------------- pkg/smartcontract/param_type.go | 269 ++++++++++++++ pkg/smartcontract/param_type_test.go | 298 +++++++++++++++ 13 files changed, 979 insertions(+), 1001 deletions(-) delete mode 100644 pkg/rpc/request/stack_param.go delete mode 100644 pkg/rpc/request/stack_param_test.go create mode 100644 pkg/smartcontract/param_type.go create mode 100644 pkg/smartcontract/param_type_test.go diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 31e8c09cf..db59a2844 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -296,10 +296,10 @@ func initSmartContract(ctx *cli.Context) error { // TODO: Fix the missing neo-go.yml file with the `init` command when the package manager is in place. if !ctx.Bool("skip-details") { details := parseContractDetails() - details.ReturnType = request.ByteArray - details.Parameters = make([]request.StackParamType, 2) - details.Parameters[0] = request.String - details.Parameters[1] = request.Array + details.ReturnType = smartcontract.ByteArrayType + details.Parameters = make([]smartcontract.ParamType, 2) + details.Parameters[0] = smartcontract.StringType + details.Parameters[1] = smartcontract.ArrayType project := &ProjectConfig{Contract: details} b, err := yaml.Marshal(project) diff --git a/pkg/rpc/request/param.go b/pkg/rpc/request/param.go index e9f453175..77dbd38bd 100644 --- a/pkg/rpc/request/param.go +++ b/pkg/rpc/request/param.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/CityOfZion/neo-go/pkg/encoding/address" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" "github.com/pkg/errors" ) @@ -24,8 +25,8 @@ type ( // FuncParam represents a function argument parameter used in the // invokefunction RPC method. FuncParam struct { - Type StackParamType `json:"type"` - Value Param `json:"value"` + Type smartcontract.ParamType `json:"type"` + Value Param `json:"value"` } ) diff --git a/pkg/rpc/request/param_test.go b/pkg/rpc/request/param_test.go index 127fab9ad..deaf36290 100644 --- a/pkg/rpc/request/param_test.go +++ b/pkg/rpc/request/param_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/CityOfZion/neo-go/pkg/encoding/address" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,7 +42,7 @@ func TestParam_UnmarshalJSON(t *testing.T) { { Type: FuncParamT, Value: FuncParam{ - Type: String, + Type: smartcontract.StringType, Value: Param{ Type: StringT, Value: "jajaja", @@ -146,7 +147,7 @@ func TestParamGetUint160FromAddress(t *testing.T) { func TestParamGetFuncParam(t *testing.T) { fp := FuncParam{ - Type: String, + Type: smartcontract.StringType, Value: Param{ Type: StringT, Value: "jajaja", diff --git a/pkg/rpc/request/scdetails.go b/pkg/rpc/request/scdetails.go index f6f910aff..f52302683 100644 --- a/pkg/rpc/request/scdetails.go +++ b/pkg/rpc/request/scdetails.go @@ -1,5 +1,7 @@ package request +import "github.com/CityOfZion/neo-go/pkg/smartcontract" + // ContractDetails contains contract metadata. type ContractDetails struct { Author string @@ -10,6 +12,6 @@ type ContractDetails struct { HasStorage bool HasDynamicInvocation bool IsPayable bool - ReturnType StackParamType - Parameters []StackParamType + ReturnType smartcontract.ParamType + Parameters []smartcontract.ParamType } diff --git a/pkg/rpc/request/stack_param.go b/pkg/rpc/request/stack_param.go deleted file mode 100644 index 192a2f0ef..000000000 --- a/pkg/rpc/request/stack_param.go +++ /dev/null @@ -1,291 +0,0 @@ -package request - -import ( - "encoding/binary" - "encoding/hex" - "encoding/json" - "strconv" - - "github.com/CityOfZion/neo-go/pkg/util" - "github.com/pkg/errors" -) - -// StackParamType represents different types of stack values. -type StackParamType int - -// All possible StackParamType values are listed here. -const ( - Unknown StackParamType = -1 - Signature StackParamType = 0x00 - Boolean StackParamType = 0x01 - Integer StackParamType = 0x02 - Hash160 StackParamType = 0x03 - Hash256 StackParamType = 0x04 - ByteArray StackParamType = 0x05 - PublicKey StackParamType = 0x06 - String StackParamType = 0x07 - Array StackParamType = 0x10 - InteropInterface StackParamType = 0xf0 - Void StackParamType = 0xff -) - -// String implements the stringer interface. -func (t StackParamType) String() string { - switch t { - case Signature: - return "Signature" - case Boolean: - return "Boolean" - case Integer: - return "Integer" - case Hash160: - return "Hash160" - case Hash256: - return "Hash256" - case ByteArray: - return "ByteArray" - case PublicKey: - return "PublicKey" - case String: - return "String" - case Array: - return "Array" - case InteropInterface: - return "InteropInterface" - case Void: - return "Void" - default: - return "Unknown" - } -} - -// StackParamTypeFromString converts string into the StackParamType. -func StackParamTypeFromString(s string) (StackParamType, error) { - switch s { - case "Signature": - return Signature, nil - case "Boolean": - return Boolean, nil - case "Integer": - return Integer, nil - case "Hash160": - return Hash160, nil - case "Hash256": - return Hash256, nil - case "ByteArray": - return ByteArray, nil - case "PublicKey": - return PublicKey, nil - case "String": - return String, nil - case "Array": - return Array, nil - case "InteropInterface": - return InteropInterface, nil - case "Void": - return Void, nil - default: - return Unknown, errors.Errorf("unknown stack parameter type: %s", s) - } -} - -// MarshalJSON implements the json.Marshaler interface. -func (t *StackParamType) MarshalJSON() ([]byte, error) { - return []byte(`"` + t.String() + `"`), nil -} - -// UnmarshalJSON sets StackParamType from JSON-encoded data. -func (t *StackParamType) UnmarshalJSON(data []byte) (err error) { - var ( - s = string(data) - l = len(s) - ) - if l < 2 || s[0] != '"' || s[l-1] != '"' { - *t = Unknown - return errors.Errorf("invalid type: %s", s) - } - *t, err = StackParamTypeFromString(s[1 : l-1]) - return -} - -// MarshalYAML implements the YAML Marshaler interface. -func (t *StackParamType) MarshalYAML() (interface{}, error) { - return t.String(), nil -} - -// UnmarshalYAML implements the YAML Unmarshaler interface. -func (t *StackParamType) UnmarshalYAML(unmarshal func(interface{}) error) error { - var name string - - err := unmarshal(&name) - if err != nil { - return err - } - *t, err = StackParamTypeFromString(name) - return err -} - -// StackParam represent a stack parameter. -type StackParam struct { - Type StackParamType `json:"type"` - Value interface{} `json:"value"` -} - -type rawStackParam struct { - Type StackParamType `json:"type"` - Value json.RawMessage `json:"value"` -} - -// UnmarshalJSON implements Unmarshaler interface. -func (p *StackParam) UnmarshalJSON(data []byte) (err error) { - var ( - r rawStackParam - i int64 - s string - b []byte - ) - - if err = json.Unmarshal(data, &r); err != nil { - return - } - - switch p.Type = r.Type; r.Type { - case ByteArray: - if err = json.Unmarshal(r.Value, &s); err != nil { - return - } - if b, err = hex.DecodeString(s); err != nil { - return - } - p.Value = b - case String: - if err = json.Unmarshal(r.Value, &s); err != nil { - return - } - p.Value = s - case Integer: - if err = json.Unmarshal(r.Value, &i); err == nil { - p.Value = i - return - } - // sometimes integer comes as string - if err = json.Unmarshal(r.Value, &s); err != nil { - return - } - if i, err = strconv.ParseInt(s, 10, 64); err != nil { - return - } - p.Value = i - case Array: - // https://github.com/neo-project/neo/blob/3d59ecca5a8deb057bdad94b3028a6d5e25ac088/neo/Network/RPC/RpcServer.cs#L67 - var rs []StackParam - if err = json.Unmarshal(r.Value, &rs); err != nil { - return - } - p.Value = rs - case Hash160: - var h util.Uint160 - if err = json.Unmarshal(r.Value, &h); err != nil { - return - } - p.Value = h - case Hash256: - var h util.Uint256 - if err = json.Unmarshal(r.Value, &h); err != nil { - return - } - p.Value = h - default: - return errors.New("not implemented") - } - return -} - -// StackParams is an array of StackParam (TODO: drop it?). -type StackParams []StackParam - -// TryParseArray converts an array of StackParam into an array of more appropriate things. -func (p StackParams) TryParseArray(vals ...interface{}) error { - var ( - err error - i int - par StackParam - ) - if len(p) != len(vals) { - return errors.New("receiver array doesn't fit the StackParams length") - } - for i, par = range p { - if err = par.TryParse(vals[i]); err != nil { - return err - } - } - return nil -} - -// TryParse converts one StackParam into something more appropriate. -func (p StackParam) TryParse(dest interface{}) error { - var ( - err error - ok bool - data []byte - ) - switch p.Type { - case ByteArray: - if data, ok = p.Value.([]byte); !ok { - return errors.Errorf("failed to cast %s to []byte", p.Value) - } - switch dest := dest.(type) { - case *util.Uint160: - if *dest, err = util.Uint160DecodeBytesBE(data); err != nil { - return err - } - return nil - case *[]byte: - *dest = data - return nil - case *util.Uint256: - if *dest, err = util.Uint256DecodeBytesLE(data); err != nil { - return err - } - return nil - case *int64, *int32, *int16, *int8, *int, *uint64, *uint32, *uint16, *uint8, *uint: - i := bytesToUint64(data) - switch dest := dest.(type) { - case *int64: - *dest = int64(i) - case *int32: - *dest = int32(i) - case *int16: - *dest = int16(i) - case *int8: - *dest = int8(i) - case *int: - *dest = int(i) - case *uint64: - *dest = i - case *uint32: - *dest = uint32(i) - case *uint16: - *dest = uint16(i) - case *uint8: - *dest = uint8(i) - case *uint: - *dest = uint(i) - } - case *string: - *dest = string(data) - return nil - default: - return errors.Errorf("cannot cast stackparam of type %s to type %s", p.Type, dest) - } - default: - return errors.New("cannot define stackparam type") - } - return nil -} - -func bytesToUint64(b []byte) uint64 { - data := make([]byte, 8) - copy(data[8-len(b):], util.ArrayReverse(b)) - return binary.BigEndian.Uint64(data) -} diff --git a/pkg/rpc/request/stack_param_test.go b/pkg/rpc/request/stack_param_test.go deleted file mode 100644 index 2d2029cff..000000000 --- a/pkg/rpc/request/stack_param_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package request - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/CityOfZion/neo-go/pkg/util" - "github.com/stretchr/testify/assert" -) - -var testCases = []struct { - input string - result StackParam -}{ - { - input: `{"type":"Integer","value":12345}`, - result: StackParam{Type: Integer, Value: int64(12345)}, - }, - { - input: `{"type":"Integer","value":"12345"}`, - result: StackParam{Type: Integer, Value: int64(12345)}, - }, - { - input: `{"type":"ByteArray","value":"010203"}`, - result: StackParam{Type: ByteArray, Value: []byte{0x01, 0x02, 0x03}}, - }, - { - input: `{"type":"String","value":"Some string"}`, - result: StackParam{Type: String, Value: "Some string"}, - }, - { - input: `{"type":"Array","value":[ -{"type": "String", "value": "str 1"}, -{"type": "Integer", "value": 2}]}`, - result: StackParam{ - Type: Array, - Value: []StackParam{ - {Type: String, Value: "str 1"}, - {Type: Integer, Value: int64(2)}, - }, - }, - }, - { - input: `{"type": "Hash160", "value": "0bcd2978634d961c24f5aea0802297ff128724d6"}`, - result: StackParam{ - Type: Hash160, - Value: util.Uint160{ - 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, - 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, - }, - }, - }, - { - input: `{"type": "Hash256", "value": "f037308fa0ab18155bccfc08485468c112409ea5064595699e98c545f245f32d"}`, - result: StackParam{ - Type: Hash256, - Value: util.Uint256{ - 0x2d, 0xf3, 0x45, 0xf2, 0x45, 0xc5, 0x98, 0x9e, - 0x69, 0x95, 0x45, 0x06, 0xa5, 0x9e, 0x40, 0x12, - 0xc1, 0x68, 0x54, 0x48, 0x08, 0xfc, 0xcc, 0x5b, - 0x15, 0x18, 0xab, 0xa0, 0x8f, 0x30, 0x37, 0xf0, - }, - }, - }, -} - -var errorCases = []string{ - `{"type": "ByteArray","value":`, // incorrect JSON - `{"type": "ByteArray","value":1}`, // incorrect Value - `{"type": "ByteArray","value":"12zz"}`, // incorrect ByteArray value - `{"type": "String","value":`, // incorrect JSON - `{"type": "String","value":1}`, // incorrect Value - `{"type": "Integer","value": "nn"}`, // incorrect Integer value - `{"type": "Integer","value": []}`, // incorrect Integer value - `{"type": "Array","value": 123}`, // incorrect Array value - `{"type": "Hash160","value": "0bcd"}`, // incorrect Uint160 value - `{"type": "Hash256","value": "0bcd"}`, // incorrect Uint256 value - `{"type": "Stringg","value": ""}`, // incorrect type - `{"type": {},"value": ""}`, // incorrect value - - `{"type": "InteropInterface","value": ""}`, // ununmarshable type -} - -func TestStackParam_UnmarshalJSON(t *testing.T) { - var s StackParam - for _, tc := range testCases { - assert.NoError(t, json.Unmarshal([]byte(tc.input), &s)) - assert.Equal(t, s, tc.result) - } - - for _, input := range errorCases { - assert.Error(t, json.Unmarshal([]byte(input), &s)) - } -} - -var tryParseTestCases = []struct { - input interface{} - expected interface{} -}{ - { - input: []byte{ - 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, - 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, - }, - expected: util.Uint160{ - 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, - 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, - }, - }, - { - input: []byte{ - 0xf0, 0x37, 0x30, 0x8f, 0xa0, 0xab, 0x18, 0x15, - 0x5b, 0xcc, 0xfc, 0x08, 0x48, 0x54, 0x68, 0xc1, - 0x12, 0x40, 0x9e, 0xa5, 0x06, 0x45, 0x95, 0x69, - 0x9e, 0x98, 0xc5, 0x45, 0xf2, 0x45, 0xf3, 0x2d, - }, - expected: util.Uint256{ - 0x2d, 0xf3, 0x45, 0xf2, 0x45, 0xc5, 0x98, 0x9e, - 0x69, 0x95, 0x45, 0x06, 0xa5, 0x9e, 0x40, 0x12, - 0xc1, 0x68, 0x54, 0x48, 0x08, 0xfc, 0xcc, 0x5b, - 0x15, 0x18, 0xab, 0xa0, 0x8f, 0x30, 0x37, 0xf0, - }, - }, - { - input: []byte{0, 1, 2, 3, 4, 9, 8, 6}, - expected: []byte{0, 1, 2, 3, 4, 9, 8, 6}, - }, - { - input: []byte{0x63, 0x78, 0x29, 0xcd, 0x0b}, - expected: int64(50686687331), - }, - { - input: []byte("this is a test string"), - expected: "this is a test string", - }, -} - -func TestStackParam_TryParse(t *testing.T) { - for _, tc := range tryParseTestCases { - t.Run(reflect.TypeOf(tc.expected).String(), func(t *testing.T) { - input := StackParam{ - Type: ByteArray, - Value: tc.input, - } - - val := reflect.New(reflect.TypeOf(tc.expected)) - assert.NoError(t, input.TryParse(val.Interface())) - assert.Equal(t, tc.expected, val.Elem().Interface()) - }) - } - - t.Run("[]Uint160", func(t *testing.T) { - exp1 := util.Uint160{1, 2, 3, 4, 5} - exp2 := util.Uint160{9, 8, 7, 6, 5} - - params := StackParams{ - { - Type: ByteArray, - Value: exp1.BytesBE(), - }, - { - Type: ByteArray, - Value: exp2.BytesBE(), - }, - } - - var out1, out2 util.Uint160 - - assert.NoError(t, params.TryParseArray(&out1, &out2)) - assert.Equal(t, exp1, out1) - assert.Equal(t, exp2, out2) - }) -} - -func TestStackParamType_String(t *testing.T) { - types := []StackParamType{ - Signature, - Boolean, - Integer, - Hash160, - Hash256, - ByteArray, - PublicKey, - String, - Array, - InteropInterface, - Void, - } - - for _, exp := range types { - actual, err := StackParamTypeFromString(exp.String()) - assert.NoError(t, err) - assert.Equal(t, exp, actual) - } - - actual, err := StackParamTypeFromString(Unknown.String()) - assert.Error(t, err) - assert.Equal(t, Unknown, actual) -} diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index df14e84c2..dad08b42a 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -148,31 +148,31 @@ func expandArrayIntoScript(script *io.BinWriter, slice []Param) error { return err } switch fp.Type { - case ByteArray, Signature: + case smartcontract.ByteArrayType, smartcontract.SignatureType: str, err := fp.Value.GetBytesHex() if err != nil { return err } emit.Bytes(script, str) - case String: + case smartcontract.StringType: str, err := fp.Value.GetString() if err != nil { return err } emit.String(script, str) - case Hash160: + case smartcontract.Hash160Type: hash, err := fp.Value.GetUint160FromHex() if err != nil { return err } emit.Bytes(script, hash.BytesBE()) - case Hash256: + case smartcontract.Hash256Type: hash, err := fp.Value.GetUint256() if err != nil { return err } emit.Bytes(script, hash.BytesBE()) - case PublicKey: + case smartcontract.PublicKeyType: str, err := fp.Value.GetString() if err != nil { return err @@ -182,13 +182,13 @@ func expandArrayIntoScript(script *io.BinWriter, slice []Param) error { return err } emit.Bytes(script, key.Bytes()) - case Integer: + case smartcontract.IntegerType: val, err := fp.Value.GetInt() if err != nil { return err } emit.Int(script, int64(val)) - case Boolean: + case smartcontract.BoolType: str, err := fp.Value.GetString() if err != nil { return err diff --git a/pkg/rpc/request/tx_builder_test.go b/pkg/rpc/request/tx_builder_test.go index a4e29edd5..8b5254e17 100644 --- a/pkg/rpc/request/tx_builder_test.go +++ b/pkg/rpc/request/tx_builder_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "testing" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,31 +30,31 @@ func TestInvocationScriptCreationGood(t *testing.T) { ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{}}}, script: "00c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: ByteArray, Value: Param{Type: StringT, Value: "50befd26fdf6e4d957c11e078b24ebce6291456f"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.ByteArrayType, Value: Param{Type: StringT, Value: "50befd26fdf6e4d957c11e078b24ebce6291456f"}}}}}}, script: "1450befd26fdf6e4d957c11e078b24ebce6291456f51c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Signature, Value: Param{Type: StringT, Value: "4edf5005771de04619235d5a4c7a9a11bb78e008541f1da7725f654c33380a3c87e2959a025da706d7255cb3a3fa07ebe9c6559d0d9e6213c68049168eb1056f"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.SignatureType, Value: Param{Type: StringT, Value: "4edf5005771de04619235d5a4c7a9a11bb78e008541f1da7725f654c33380a3c87e2959a025da706d7255cb3a3fa07ebe9c6559d0d9e6213c68049168eb1056f"}}}}}}, script: "404edf5005771de04619235d5a4c7a9a11bb78e008541f1da7725f654c33380a3c87e2959a025da706d7255cb3a3fa07ebe9c6559d0d9e6213c68049168eb1056f51c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: String, Value: Param{Type: StringT, Value: "50befd26fdf6e4d957c11e078b24ebce6291456f"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.StringType, Value: Param{Type: StringT, Value: "50befd26fdf6e4d957c11e078b24ebce6291456f"}}}}}}, script: "283530626566643236666466366534643935376331316530373862323465626365363239313435366651c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Hash160, Value: Param{Type: StringT, Value: "50befd26fdf6e4d957c11e078b24ebce6291456f"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.Hash160Type, Value: Param{Type: StringT, Value: "50befd26fdf6e4d957c11e078b24ebce6291456f"}}}}}}, script: "146f459162ceeb248b071ec157d9e4f6fd26fdbe5051c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Hash256, Value: Param{Type: StringT, Value: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.Hash256Type, Value: Param{Type: StringT, Value: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"}}}}}}, script: "20e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6051c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: PublicKey, Value: Param{Type: StringT, Value: "03c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.PublicKeyType, Value: Param{Type: StringT, Value: "03c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1"}}}}}}, script: "2103c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c151c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Integer, Value: Param{Type: NumberT, Value: 42}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.IntegerType, Value: Param{Type: NumberT, Value: 42}}}}}}, script: "012a51c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Boolean, Value: Param{Type: StringT, Value: "true"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.BoolType, Value: Param{Type: StringT, Value: "true"}}}}}}, script: "5151c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }, { - ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Boolean, Value: Param{Type: StringT, Value: "false"}}}}}}, + ps: Params{{Type: StringT, Value: "a"}, {Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.BoolType, Value: Param{Type: StringT, Value: "false"}}}}}}, script: "0051c10161676f459162ceeb248b071ec157d9e4f6fd26fdbe50", }} for _, ps := range paramScripts { @@ -70,17 +71,17 @@ func TestInvocationScriptCreationBad(t *testing.T) { {{Type: NumberT, Value: "qwerty"}}, {{Type: ArrayT, Value: 42}}, {{Type: ArrayT, Value: []Param{{Type: NumberT, Value: 42}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: ByteArray, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Signature, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: String, Value: Param{Type: NumberT, Value: 42}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Hash160, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Hash256, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: PublicKey, Value: Param{Type: NumberT, Value: 42}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: PublicKey, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Integer, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Boolean, Value: Param{Type: NumberT, Value: 42}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Boolean, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, - {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: Unknown, Value: Param{}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.ByteArrayType, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.SignatureType, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.StringType, Value: Param{Type: NumberT, Value: 42}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.Hash160Type, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.Hash256Type, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.PublicKeyType, Value: Param{Type: NumberT, Value: 42}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.PublicKeyType, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.IntegerType, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.BoolType, Value: Param{Type: NumberT, Value: 42}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.BoolType, Value: Param{Type: StringT, Value: "qwerty"}}}}}}, + {{Type: ArrayT, Value: []Param{{Type: FuncParamT, Value: FuncParam{Type: smartcontract.UnknownType, Value: Param{}}}}}}, } for _, ps := range testParams { _, err := CreateFunctionInvocationScript(contract, ps) diff --git a/pkg/rpc/response/types.go b/pkg/rpc/response/types.go index 6b13cb018..a655d6bab 100644 --- a/pkg/rpc/response/types.go +++ b/pkg/rpc/response/types.go @@ -3,8 +3,8 @@ package response import ( "encoding/json" - "github.com/CityOfZion/neo-go/pkg/rpc/request" "github.com/CityOfZion/neo-go/pkg/rpc/response/result" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/vm" ) @@ -14,7 +14,7 @@ type InvokeResult struct { State vm.State `json:"state"` GasConsumed string `json:"gas_consumed"` Script string `json:"script"` - Stack []request.StackParam + Stack []smartcontract.Parameter } // Header is a generic JSON-RPC 2.0 response header (ID and JSON-RPC version). diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index a60d13c1e..fe8a2561b 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -1,33 +1,16 @@ package smartcontract import ( + "encoding/binary" "encoding/hex" "encoding/json" - "errors" + "math/bits" "strconv" "strings" "unicode/utf8" - "github.com/CityOfZion/neo-go/pkg/crypto/keys" - "github.com/CityOfZion/neo-go/pkg/encoding/address" - "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" -) - -// ParamType represents the Type of the contract parameter. -type ParamType byte - -// A list of supported smart contract parameter types. -const ( - SignatureType ParamType = iota - BoolType - IntegerType - Hash160Type - Hash256Type - ByteArrayType - PublicKeyType - StringType - ArrayType + "github.com/pkg/errors" ) // PropertyState represents contract properties (flags). @@ -49,62 +32,6 @@ type Parameter struct { Value interface{} `json:"value"` } -func (pt ParamType) String() string { - switch pt { - case SignatureType: - return "Signature" - case BoolType: - return "Boolean" - case IntegerType: - return "Integer" - case Hash160Type: - return "Hash160" - case Hash256Type: - return "Hash256" - case ByteArrayType: - return "ByteArray" - case PublicKeyType: - return "PublicKey" - case StringType: - return "String" - case ArrayType: - return "Array" - default: - return "" - } -} - -// MarshalJSON implements the json.Marshaler interface. -func (pt ParamType) MarshalJSON() ([]byte, error) { - return []byte(`"` + pt.String() + `"`), nil -} - -// UnmarshalJSON implements json.Unmarshaler interface. -func (pt *ParamType) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - p, err := parseParamType(s) - if err != nil { - return err - } - - *pt = p - return nil -} - -// EncodeBinary implements io.Serializable interface. -func (pt ParamType) EncodeBinary(w *io.BinWriter) { - w.WriteB(byte(pt)) -} - -// DecodeBinary implements io.Serializable interface. -func (pt *ParamType) DecodeBinary(r *io.BinReader) { - *pt = ParamType(r.ReadB()) -} - // NewParameter returns a Parameter with proper initialized Value // of the given ParamType. func NewParameter(t ParamType) Parameter { @@ -114,149 +41,188 @@ func NewParameter(t ParamType) Parameter { } } -// parseParamType is a user-friendly string to ParamType converter, it's -// case-insensitive and makes the following conversions: -// signature -> SignatureType -// bool -> BoolType -// int -> IntegerType -// hash160 -> Hash160Type -// hash256 -> Hash256Type -// bytes -> ByteArrayType -// key -> PublicKeyType -// string -> StringType -// anything else generates an error. -func parseParamType(typ string) (ParamType, error) { - switch strings.ToLower(typ) { - case "signature": - return SignatureType, nil - case "bool": - return BoolType, nil - case "int": - return IntegerType, nil - case "hash160": - return Hash160Type, nil - case "hash256": - return Hash256Type, nil - case "bytes", "bytearray": - return ByteArrayType, nil - case "key": - return PublicKeyType, nil - case "string": - return StringType, nil - default: - // We deliberately don't support array here. - return 0, errors.New("wrong or unsupported parameter type") - } +type rawParameter struct { + Type ParamType `json:"type"` + Value json.RawMessage `json:"value"` } -// adjustValToType is a value type-checker and converter. -func adjustValToType(typ ParamType, val string) (interface{}, error) { - switch typ { - case SignatureType: - b, err := hex.DecodeString(val) - if err != nil { - return nil, err - } - if len(b) != 64 { - return nil, errors.New("not a signature") - } - return val, nil - case BoolType: - switch val { - case "true": - return true, nil - case "false": - return false, nil - default: - return nil, errors.New("invalid boolean value") - } - case IntegerType: - return strconv.Atoi(val) - case Hash160Type: - u, err := address.StringToUint160(val) - if err == nil { - return hex.EncodeToString(u.BytesBE()), nil - } - b, err := hex.DecodeString(val) - if err != nil { - return nil, err - } - if len(b) != 20 { - return nil, errors.New("not a hash160") - } - return val, nil - case Hash256Type: - b, err := hex.DecodeString(val) - if err != nil { - return nil, err - } - if len(b) != 32 { - return nil, errors.New("not a hash256") - } - return val, nil +// UnmarshalJSON implements Unmarshaler interface. +func (p *Parameter) UnmarshalJSON(data []byte) (err error) { + var ( + r rawParameter + i int64 + s string + b []byte + ) + + if err = json.Unmarshal(data, &r); err != nil { + return + } + + switch p.Type = r.Type; r.Type { case ByteArrayType: - _, err := hex.DecodeString(val) - if err != nil { - return nil, err + if err = json.Unmarshal(r.Value, &s); err != nil { + return } - return val, nil - case PublicKeyType: - _, err := keys.NewPublicKeyFromString(val) - if err != nil { - return nil, err + if b, err = hex.DecodeString(s); err != nil { + return } - return val, nil + p.Value = b case StringType: - return val, nil + if err = json.Unmarshal(r.Value, &s); err != nil { + return + } + p.Value = s + case IntegerType: + if err = json.Unmarshal(r.Value, &i); err == nil { + p.Value = i + return + } + // sometimes integer comes as string + if err = json.Unmarshal(r.Value, &s); err != nil { + return + } + if i, err = strconv.ParseInt(s, 10, 64); err != nil { + return + } + p.Value = i + case ArrayType: + // https://github.com/neo-project/neo/blob/3d59ecca5a8deb057bdad94b3028a6d5e25ac088/neo/Network/RPC/RpcServer.cs#L67 + var rs []Parameter + if err = json.Unmarshal(r.Value, &rs); err != nil { + return + } + p.Value = rs + case Hash160Type: + var h util.Uint160 + if err = json.Unmarshal(r.Value, &h); err != nil { + return + } + p.Value = h + case Hash256Type: + var h util.Uint256 + if err = json.Unmarshal(r.Value, &h); err != nil { + return + } + p.Value = h default: - return nil, errors.New("unsupported parameter type") + return errors.New("not implemented") } + return } -// inferParamType tries to infer the value type from its contents. It returns -// IntegerType for anything that looks like decimal integer (can be converted -// with strconv.Atoi), BoolType for true and false values, Hash160Type for -// addresses and hex strings encoding 20 bytes long values, PublicKeyType for -// valid hex-encoded public keys, Hash256Type for hex-encoded 32 bytes values, -// SignatureType for hex-encoded 64 bytes values, ByteArrayType for any other -// valid hex-encoded values and StringType for anything else. -func inferParamType(val string) ParamType { - var err error +// Params is an array of Parameter (TODO: drop it?). +type Params []Parameter - _, err = strconv.Atoi(val) - if err == nil { - return IntegerType +// TryParseArray converts an array of Parameter into an array of more appropriate things. +func (p Params) TryParseArray(vals ...interface{}) error { + var ( + err error + i int + par Parameter + ) + if len(p) != len(vals) { + return errors.New("receiver array doesn't fit the Params length") } - - if val == "true" || val == "false" { - return BoolType - } - - _, err = address.StringToUint160(val) - if err == nil { - return Hash160Type - } - - _, err = keys.NewPublicKeyFromString(val) - if err == nil { - return PublicKeyType - } - - unhexed, err := hex.DecodeString(val) - if err == nil { - switch len(unhexed) { - case 20: - return Hash160Type - case 32: - return Hash256Type - case 64: - return SignatureType - default: - return ByteArrayType + for i, par = range p { + if err = par.TryParse(vals[i]); err != nil { + return err } } - // Anything can be a string. - return StringType + return nil +} + +// TryParse converts one Parameter into something more appropriate. +func (p Parameter) TryParse(dest interface{}) error { + var ( + err error + ok bool + data []byte + ) + switch p.Type { + case ByteArrayType: + if data, ok = p.Value.([]byte); !ok { + return errors.Errorf("failed to cast %s to []byte", p.Value) + } + switch dest := dest.(type) { + case *util.Uint160: + if *dest, err = util.Uint160DecodeBytesBE(data); err != nil { + return err + } + return nil + case *[]byte: + *dest = data + return nil + case *util.Uint256: + if *dest, err = util.Uint256DecodeBytesLE(data); err != nil { + return err + } + return nil + case *int64, *int32, *int16, *int8, *int, *uint64, *uint32, *uint16, *uint8, *uint: + var size int + switch dest.(type) { + case *int64, *uint64: + size = 64 + case *int32, *uint32: + size = 32 + case *int16, *uint16: + size = 16 + case *int8, *uint8: + size = 8 + case *int, *uint: + size = bits.UintSize + } + + i, err := bytesToUint64(data, size) + if err != nil { + return err + } + + switch dest := dest.(type) { + case *int64: + *dest = int64(i) + case *int32: + *dest = int32(i) + case *int16: + *dest = int16(i) + case *int8: + *dest = int8(i) + case *int: + *dest = int(i) + case *uint64: + *dest = i + case *uint32: + *dest = uint32(i) + case *uint16: + *dest = uint16(i) + case *uint8: + *dest = uint8(i) + case *uint: + *dest = uint(i) + } + case *string: + *dest = string(data) + return nil + default: + return errors.Errorf("cannot cast param of type %s to type %s", p.Type, dest) + } + default: + return errors.New("cannot define param type") + } + return nil +} + +func bytesToUint64(b []byte, size int) (uint64, error) { + var length = size / 8 + if len(b) > length { + return 0, errors.Errorf("input doesn't fit into %d bits", size) + } + if len(b) < length { + data := make([]byte, length) + copy(data, b) + return binary.LittleEndian.Uint64(data), nil + } + return binary.LittleEndian.Uint64(b), nil } // NewParameterFromString returns a new Parameter initialized from the given @@ -282,10 +248,14 @@ func NewParameterFromString(in string) (*Parameter, error) { } if char == ':' && !escaped && !hadType { typStr := buf.String() - res.Type, err = parseParamType(typStr) + res.Type, err = ParseParamType(typStr) if err != nil { return nil, err } + // We currently do not support following types: + if res.Type == ArrayType || res.Type == MapType || res.Type == InteropInterfaceType || res.Type == VoidType { + return nil, errors.Errorf("Unsupported contract parameter type: %s", res.Type) + } buf.Reset() hadType = true continue diff --git a/pkg/smartcontract/param_context_test.go b/pkg/smartcontract/param_context_test.go index 1f95d3128..288c8558c 100644 --- a/pkg/smartcontract/param_context_test.go +++ b/pkg/smartcontract/param_context_test.go @@ -1,283 +1,204 @@ package smartcontract import ( + "encoding/json" + "reflect" "testing" + "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/assert" ) -func TestParseParamType(t *testing.T) { - var inouts = []struct { - in string - out ParamType - err bool - }{{ - in: "signature", - out: SignatureType, - }, { - in: "Signature", - out: SignatureType, - }, { - in: "SiGnAtUrE", - out: SignatureType, - }, { - in: "bool", - out: BoolType, - }, { - in: "int", - out: IntegerType, - }, { - in: "hash160", - out: Hash160Type, - }, { - in: "hash256", - out: Hash256Type, - }, { - in: "bytes", - out: ByteArrayType, - }, { - in: "key", - out: PublicKeyType, - }, { - in: "string", - out: StringType, - }, { - in: "array", - err: true, - }, { - in: "qwerty", - err: true, - }} - for _, inout := range inouts { - out, err := parseParamType(inout.in) - if inout.err { - assert.NotNil(t, err, "should error on '%s' input", inout.in) - } else { - assert.Nil(t, err, "shouldn't error on '%s' input", inout.in) - assert.Equal(t, inout.out, out, "bad output for '%s' input", inout.in) - } +var testCases = []struct { + input string + result Parameter +}{ + { + input: `{"type":"Integer","value":12345}`, + result: Parameter{Type: IntegerType, Value: int64(12345)}, + }, + { + input: `{"type":"Integer","value":"12345"}`, + result: Parameter{Type: IntegerType, Value: int64(12345)}, + }, + { + input: `{"type":"ByteArray","value":"010203"}`, + result: Parameter{Type: ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, + }, + { + input: `{"type":"String","value":"Some string"}`, + result: Parameter{Type: StringType, Value: "Some string"}, + }, + { + input: `{"type":"Array","value":[ + {"type": "String", "value": "str 1"}, + {"type": "Integer", "value": 2}]}`, + result: Parameter{ + Type: ArrayType, + Value: []Parameter{ + {Type: StringType, Value: "str 1"}, + {Type: IntegerType, Value: int64(2)}, + }, + }, + }, + { + input: `{"type": "Hash160", "value": "0bcd2978634d961c24f5aea0802297ff128724d6"}`, + result: Parameter{ + Type: Hash160Type, + Value: util.Uint160{ + 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, + 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, + }, + }, + }, + { + input: `{"type": "Hash256", "value": "f037308fa0ab18155bccfc08485468c112409ea5064595699e98c545f245f32d"}`, + result: Parameter{ + Type: Hash256Type, + Value: util.Uint256{ + 0x2d, 0xf3, 0x45, 0xf2, 0x45, 0xc5, 0x98, 0x9e, + 0x69, 0x95, 0x45, 0x06, 0xa5, 0x9e, 0x40, 0x12, + 0xc1, 0x68, 0x54, 0x48, 0x08, 0xfc, 0xcc, 0x5b, + 0x15, 0x18, 0xab, 0xa0, 0x8f, 0x30, 0x37, 0xf0, + }, + }, + }, +} + +var errorCases = []string{ + `{"type": "ByteArray","value":`, // incorrect JSON + `{"type": "ByteArray","value":1}`, // incorrect Value + `{"type": "ByteArray","value":"12zz"}`, // incorrect ByteArray value + `{"type": "String","value":`, // incorrect JSON + `{"type": "String","value":1}`, // incorrect Value + `{"type": "Integer","value": "nn"}`, // incorrect Integer value + `{"type": "Integer","value": []}`, // incorrect Integer value + `{"type": "Array","value": 123}`, // incorrect Array value + `{"type": "Hash160","value": "0bcd"}`, // incorrect Uint160 value + `{"type": "Hash256","value": "0bcd"}`, // incorrect Uint256 value + `{"type": "Stringg","value": ""}`, // incorrect type + `{"type": {},"value": ""}`, // incorrect value + + `{"type": "InteropInterface","value": ""}`, // ununmarshable type + `{"type": "Map","value": ""}`, //unmarshable type +} + +func TestParam_UnmarshalJSON(t *testing.T) { + var s Parameter + for _, tc := range testCases { + assert.NoError(t, json.Unmarshal([]byte(tc.input), &s)) + assert.Equal(t, s, tc.result) + } + + for _, input := range errorCases { + assert.Error(t, json.Unmarshal([]byte(input), &s)) } } -func TestInferParamType(t *testing.T) { - var inouts = []struct { - in string - out ParamType - }{{ - in: "42", - out: IntegerType, - }, { - in: "-42", - out: IntegerType, - }, { - in: "0", - out: IntegerType, - }, { - in: "2e10", - out: ByteArrayType, - }, { - in: "true", - out: BoolType, - }, { - in: "false", - out: BoolType, - }, { - in: "truee", - out: StringType, - }, { - in: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", - out: Hash160Type, - }, { - in: "ZK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", - out: StringType, - }, { - in: "50befd26fdf6e4d957c11e078b24ebce6291456f", - out: Hash160Type, - }, { - in: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", - out: PublicKeyType, - }, { - in: "30b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", - out: ByteArrayType, - }, { - in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", - out: Hash256Type, - }, { - in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7da", - out: ByteArrayType, - }, { - in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", - out: SignatureType, - }, { - in: "qwerty", - out: StringType, - }, { - in: "ab", - out: ByteArrayType, - }, { - in: "az", - out: StringType, - }, { - in: "bad", - out: StringType, - }, { - in: "фыва", - out: StringType, - }, { - in: "dead", - out: ByteArrayType, - }} - for _, inout := range inouts { - out := inferParamType(inout.in) - assert.Equal(t, inout.out, out, "bad output for '%s' input", inout.in) - } +var tryParseTestCases = []struct { + input interface{} + expected interface{} +}{ + { + input: []byte{ + 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, + 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, + }, + expected: util.Uint160{ + 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, + 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, + }, + }, + { + input: []byte{ + 0xf0, 0x37, 0x30, 0x8f, 0xa0, 0xab, 0x18, 0x15, + 0x5b, 0xcc, 0xfc, 0x08, 0x48, 0x54, 0x68, 0xc1, + 0x12, 0x40, 0x9e, 0xa5, 0x06, 0x45, 0x95, 0x69, + 0x9e, 0x98, 0xc5, 0x45, 0xf2, 0x45, 0xf3, 0x2d, + }, + expected: util.Uint256{ + 0x2d, 0xf3, 0x45, 0xf2, 0x45, 0xc5, 0x98, 0x9e, + 0x69, 0x95, 0x45, 0x06, 0xa5, 0x9e, 0x40, 0x12, + 0xc1, 0x68, 0x54, 0x48, 0x08, 0xfc, 0xcc, 0x5b, + 0x15, 0x18, 0xab, 0xa0, 0x8f, 0x30, 0x37, 0xf0, + }, + }, + { + input: []byte{0, 1, 2, 3, 4, 9, 8, 6}, + expected: []byte{0, 1, 2, 3, 4, 9, 8, 6}, + }, + { + input: []byte{0x63, 0x78, 0x29, 0xcd, 0x0b}, + expected: int64(50686687331), + }, + { + input: []byte("this is a test string"), + expected: "this is a test string", + }, } -func TestAdjustValToType(t *testing.T) { - var inouts = []struct { - typ ParamType - val string - out interface{} - err bool - }{{ - typ: SignatureType, - val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", - out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", - }, { - typ: SignatureType, - val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c", - err: true, - }, { - typ: SignatureType, - val: "qwerty", - err: true, - }, { - typ: BoolType, - val: "false", - out: false, - }, { - typ: BoolType, - val: "true", - out: true, - }, { - typ: BoolType, - val: "qwerty", - err: true, - }, { - typ: BoolType, - val: "42", - err: true, - }, { - typ: BoolType, - val: "0", - err: true, - }, { - typ: IntegerType, - val: "0", - out: 0, - }, { - typ: IntegerType, - val: "42", - out: 42, - }, { - typ: IntegerType, - val: "-42", - out: -42, - }, { - typ: IntegerType, - val: "q", - err: true, - }, { - typ: Hash160Type, - val: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", - out: "23ba2703c53263e8d6e522dc32203339dcd8eee9", - }, { - typ: Hash160Type, - val: "50befd26fdf6e4d957c11e078b24ebce6291456f", - out: "50befd26fdf6e4d957c11e078b24ebce6291456f", - }, { - typ: Hash160Type, - val: "befd26fdf6e4d957c11e078b24ebce6291456f", - err: true, - }, { - typ: Hash160Type, - val: "q", - err: true, - }, { - typ: Hash256Type, - val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", - out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", - }, { - typ: Hash256Type, - val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d", - err: true, - }, { - typ: Hash256Type, - val: "q", - err: true, - }, { - typ: ByteArrayType, - val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d", - out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d", - }, { - typ: ByteArrayType, - val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", - out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", - }, { - typ: ByteArrayType, - val: "50befd26fdf6e4d957c11e078b24ebce6291456f", - out: "50befd26fdf6e4d957c11e078b24ebce6291456f", - }, { - typ: ByteArrayType, - val: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", - err: true, - }, { - typ: ByteArrayType, - val: "q", - err: true, - }, { - typ: ByteArrayType, - val: "ab", - out: "ab", - }, { - typ: PublicKeyType, - val: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", - out: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", - }, { - typ: PublicKeyType, - val: "01b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", - err: true, - }, { - typ: PublicKeyType, - val: "q", - err: true, - }, { - typ: StringType, - val: "q", - out: "q", - }, { - typ: StringType, - val: "dead", - out: "dead", - }, { - typ: StringType, - val: "йцукен", - out: "йцукен", - }, { - typ: ArrayType, - val: "", - err: true, - }} +func TestParam_TryParse(t *testing.T) { + for _, tc := range tryParseTestCases { + t.Run(reflect.TypeOf(tc.expected).String(), func(t *testing.T) { + input := Parameter{ + Type: ByteArrayType, + Value: tc.input, + } - for _, inout := range inouts { - out, err := adjustValToType(inout.typ, inout.val) - if inout.err { - assert.NotNil(t, err, "should error on '%s/%s' input", inout.typ, inout.val) - } else { - assert.Nil(t, err, "shouldn't error on '%s/%s' input", inout.typ, inout.val) - assert.Equal(t, inout.out, out, "bad output for '%s/%s' input", inout.typ, inout.val) - } + val := reflect.New(reflect.TypeOf(tc.expected)) + assert.NoError(t, input.TryParse(val.Interface())) + assert.Equal(t, tc.expected, val.Elem().Interface()) + }) } + + t.Run("[]Uint160", func(t *testing.T) { + exp1 := util.Uint160{1, 2, 3, 4, 5} + exp2 := util.Uint160{9, 8, 7, 6, 5} + + params := Params{ + { + Type: ByteArrayType, + Value: exp1.BytesBE(), + }, + { + Type: ByteArrayType, + Value: exp2.BytesBE(), + }, + } + + var out1, out2 util.Uint160 + + assert.NoError(t, params.TryParseArray(&out1, &out2)) + assert.Equal(t, exp1, out1) + assert.Equal(t, exp2, out2) + }) +} + +func TestParamType_String(t *testing.T) { + types := []ParamType{ + SignatureType, + BoolType, + IntegerType, + Hash160Type, + Hash256Type, + ByteArrayType, + PublicKeyType, + StringType, + ArrayType, + InteropInterfaceType, + MapType, + VoidType, + } + + for _, exp := range types { + actual, err := ParseParamType(exp.String()) + assert.NoError(t, err) + assert.Equal(t, exp, actual) + } + + actual, err := ParseParamType(UnknownType.String()) + assert.Error(t, err) + assert.Equal(t, UnknownType, actual) } func TestNewParameterFromString(t *testing.T) { @@ -330,6 +251,12 @@ func TestNewParameterFromString(t *testing.T) { }, { in: `bool:asdf`, err: true, + }, { + in: `InteropInterface:123`, + err: true, + }, { + in: `Map:[]`, + err: true, }} for _, inout := range inouts { out, err := NewParameterFromString(inout.in) diff --git a/pkg/smartcontract/param_type.go b/pkg/smartcontract/param_type.go new file mode 100644 index 000000000..e253ba846 --- /dev/null +++ b/pkg/smartcontract/param_type.go @@ -0,0 +1,269 @@ +package smartcontract + +import ( + "encoding/hex" + "encoding/json" + "strconv" + "strings" + + "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/encoding/address" + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/pkg/errors" +) + +// ParamType represents the Type of the smart contract parameter. +type ParamType int + +// A list of supported smart contract parameter types. +const ( + UnknownType ParamType = -1 + SignatureType ParamType = 0x00 + BoolType ParamType = 0x01 + IntegerType ParamType = 0x02 + Hash160Type ParamType = 0x03 + Hash256Type ParamType = 0x04 + ByteArrayType ParamType = 0x05 + PublicKeyType ParamType = 0x06 + StringType ParamType = 0x07 + ArrayType ParamType = 0x10 + MapType ParamType = 0x12 + InteropInterfaceType ParamType = 0xf0 + VoidType ParamType = 0xff +) + +// String implements the stringer interface. +func (pt ParamType) String() string { + switch pt { + case SignatureType: + return "Signature" + case BoolType: + return "Boolean" + case IntegerType: + return "Integer" + case Hash160Type: + return "Hash160" + case Hash256Type: + return "Hash256" + case ByteArrayType: + return "ByteArray" + case PublicKeyType: + return "PublicKey" + case StringType: + return "String" + case ArrayType: + return "Array" + case MapType: + return "Map" + case InteropInterfaceType: + return "InteropInterface" + case VoidType: + return "Void" + default: + return "" + } +} + +// MarshalJSON implements the json.Marshaler interface. +func (pt ParamType) MarshalJSON() ([]byte, error) { + return []byte(`"` + pt.String() + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (pt *ParamType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + p, err := ParseParamType(s) + if err != nil { + return err + } + + *pt = p + return nil +} + +// MarshalYAML implements the YAML Marshaler interface. +func (pt *ParamType) MarshalYAML() (interface{}, error) { + return pt.String(), nil +} + +// UnmarshalYAML implements the YAML Unmarshaler interface. +func (pt *ParamType) UnmarshalYAML(unmarshal func(interface{}) error) error { + var name string + + err := unmarshal(&name) + if err != nil { + return err + } + *pt, err = ParseParamType(name) + return err +} + +// EncodeBinary implements io.Serializable interface. +func (pt ParamType) EncodeBinary(w *io.BinWriter) { + w.WriteB(byte(pt)) +} + +// DecodeBinary implements io.Serializable interface. +func (pt *ParamType) DecodeBinary(r *io.BinReader) { + *pt = ParamType(r.ReadB()) +} + +// ParseParamType is a user-friendly string to ParamType converter, it's +// case-insensitive and makes the following conversions: +// signature -> SignatureType +// bool, boolean -> BoolType +// int, integer -> IntegerType +// hash160 -> Hash160Type +// hash256 -> Hash256Type +// bytes, bytearray -> ByteArrayType +// key, publickey -> PublicKeyType +// string -> StringType +// array -> ArrayType +// map -> MapType +// interopinterface -> InteropInterfaceType +// void -> VoidType +// anything else generates an error. +func ParseParamType(typ string) (ParamType, error) { + switch strings.ToLower(typ) { + case "signature": + return SignatureType, nil + case "bool", "boolean": + return BoolType, nil + case "int", "integer": + return IntegerType, nil + case "hash160": + return Hash160Type, nil + case "hash256": + return Hash256Type, nil + case "bytes", "bytearray": + return ByteArrayType, nil + case "key", "publickey": + return PublicKeyType, nil + case "string": + return StringType, nil + case "array": + return ArrayType, nil + case "map": + return MapType, nil + case "interopinterface": + return InteropInterfaceType, nil + case "void": + return VoidType, nil + default: + return UnknownType, errors.Errorf("Unknown contract parameter type: %s", typ) + } +} + +// adjustValToType is a value type-checker and converter. +func adjustValToType(typ ParamType, val string) (interface{}, error) { + switch typ { + case SignatureType: + b, err := hex.DecodeString(val) + if err != nil { + return nil, err + } + if len(b) != 64 { + return nil, errors.New("not a signature") + } + return val, nil + case BoolType: + switch val { + case "true": + return true, nil + case "false": + return false, nil + default: + return nil, errors.New("invalid boolean value") + } + case IntegerType: + return strconv.Atoi(val) + case Hash160Type: + u, err := address.StringToUint160(val) + if err == nil { + return hex.EncodeToString(u.BytesBE()), nil + } + b, err := hex.DecodeString(val) + if err != nil { + return nil, err + } + if len(b) != 20 { + return nil, errors.New("not a hash160") + } + return val, nil + case Hash256Type: + b, err := hex.DecodeString(val) + if err != nil { + return nil, err + } + if len(b) != 32 { + return nil, errors.New("not a hash256") + } + return val, nil + case ByteArrayType: + _, err := hex.DecodeString(val) + if err != nil { + return nil, err + } + return val, nil + case PublicKeyType: + _, err := keys.NewPublicKeyFromString(val) + if err != nil { + return nil, err + } + return val, nil + case StringType: + return val, nil + default: + return nil, errors.New("unsupported parameter type") + } +} + +// inferParamType tries to infer the value type from its contents. It returns +// IntegerType for anything that looks like decimal integer (can be converted +// with strconv.Atoi), BoolType for true and false values, Hash160Type for +// addresses and hex strings encoding 20 bytes long values, PublicKeyType for +// valid hex-encoded public keys, Hash256Type for hex-encoded 32 bytes values, +// SignatureType for hex-encoded 64 bytes values, ByteArrayType for any other +// valid hex-encoded values and StringType for anything else. +func inferParamType(val string) ParamType { + var err error + + _, err = strconv.Atoi(val) + if err == nil { + return IntegerType + } + + if val == "true" || val == "false" { + return BoolType + } + + _, err = address.StringToUint160(val) + if err == nil { + return Hash160Type + } + + _, err = keys.NewPublicKeyFromString(val) + if err == nil { + return PublicKeyType + } + + unhexed, err := hex.DecodeString(val) + if err == nil { + switch len(unhexed) { + case 20: + return Hash160Type + case 32: + return Hash256Type + case 64: + return SignatureType + default: + return ByteArrayType + } + } + // Anything can be a string. + return StringType +} diff --git a/pkg/smartcontract/param_type_test.go b/pkg/smartcontract/param_type_test.go new file mode 100644 index 000000000..629f1eca5 --- /dev/null +++ b/pkg/smartcontract/param_type_test.go @@ -0,0 +1,298 @@ +package smartcontract + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseParamType(t *testing.T) { + var inouts = []struct { + in string + out ParamType + err bool + }{{ + in: "signature", + out: SignatureType, + }, { + in: "Signature", + out: SignatureType, + }, { + in: "SiGnAtUrE", + out: SignatureType, + }, { + in: "bool", + out: BoolType, + }, { + in: "int", + out: IntegerType, + }, { + in: "hash160", + out: Hash160Type, + }, { + in: "hash256", + out: Hash256Type, + }, { + in: "bytes", + out: ByteArrayType, + }, { + in: "key", + out: PublicKeyType, + }, { + in: "string", + out: StringType, + }, { + in: "array", + out: ArrayType, + }, { + in: "map", + out: MapType, + }, { + in: "interopinterface", + out: InteropInterfaceType, + }, { + in: "void", + out: VoidType, + }, { + in: "qwerty", + err: true, + }} + for _, inout := range inouts { + out, err := ParseParamType(inout.in) + if inout.err { + assert.NotNil(t, err, "should error on '%s' input", inout.in) + } else { + assert.Nil(t, err, "shouldn't error on '%s' input", inout.in) + assert.Equal(t, inout.out, out, "bad output for '%s' input", inout.in) + } + } +} + +func TestInferParamType(t *testing.T) { + var inouts = []struct { + in string + out ParamType + }{{ + in: "42", + out: IntegerType, + }, { + in: "-42", + out: IntegerType, + }, { + in: "0", + out: IntegerType, + }, { + in: "2e10", + out: ByteArrayType, + }, { + in: "true", + out: BoolType, + }, { + in: "false", + out: BoolType, + }, { + in: "truee", + out: StringType, + }, { + in: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", + out: Hash160Type, + }, { + in: "ZK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", + out: StringType, + }, { + in: "50befd26fdf6e4d957c11e078b24ebce6291456f", + out: Hash160Type, + }, { + in: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + out: PublicKeyType, + }, { + in: "30b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + out: ByteArrayType, + }, { + in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + out: Hash256Type, + }, { + in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7da", + out: ByteArrayType, + }, { + in: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + out: SignatureType, + }, { + in: "qwerty", + out: StringType, + }, { + in: "ab", + out: ByteArrayType, + }, { + in: "az", + out: StringType, + }, { + in: "bad", + out: StringType, + }, { + in: "фыва", + out: StringType, + }, { + in: "dead", + out: ByteArrayType, + }} + for _, inout := range inouts { + out := inferParamType(inout.in) + assert.Equal(t, inout.out, out, "bad output for '%s' input", inout.in) + } +} + +func TestAdjustValToType(t *testing.T) { + var inouts = []struct { + typ ParamType + val string + out interface{} + err bool + }{{ + typ: SignatureType, + val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + }, { + typ: SignatureType, + val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c", + err: true, + }, { + typ: SignatureType, + val: "qwerty", + err: true, + }, { + typ: BoolType, + val: "false", + out: false, + }, { + typ: BoolType, + val: "true", + out: true, + }, { + typ: BoolType, + val: "qwerty", + err: true, + }, { + typ: BoolType, + val: "42", + err: true, + }, { + typ: BoolType, + val: "0", + err: true, + }, { + typ: IntegerType, + val: "0", + out: 0, + }, { + typ: IntegerType, + val: "42", + out: 42, + }, { + typ: IntegerType, + val: "-42", + out: -42, + }, { + typ: IntegerType, + val: "q", + err: true, + }, { + typ: Hash160Type, + val: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", + out: "23ba2703c53263e8d6e522dc32203339dcd8eee9", + }, { + typ: Hash160Type, + val: "50befd26fdf6e4d957c11e078b24ebce6291456f", + out: "50befd26fdf6e4d957c11e078b24ebce6291456f", + }, { + typ: Hash160Type, + val: "befd26fdf6e4d957c11e078b24ebce6291456f", + err: true, + }, { + typ: Hash160Type, + val: "q", + err: true, + }, { + typ: Hash256Type, + val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + }, { + typ: Hash256Type, + val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d", + err: true, + }, { + typ: Hash256Type, + val: "q", + err: true, + }, { + typ: ByteArrayType, + val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d", + out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282d", + }, { + typ: ByteArrayType, + val: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + out: "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + }, { + typ: ByteArrayType, + val: "50befd26fdf6e4d957c11e078b24ebce6291456f", + out: "50befd26fdf6e4d957c11e078b24ebce6291456f", + }, { + typ: ByteArrayType, + val: "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", + err: true, + }, { + typ: ByteArrayType, + val: "q", + err: true, + }, { + typ: ByteArrayType, + val: "ab", + out: "ab", + }, { + typ: PublicKeyType, + val: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + out: "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + }, { + typ: PublicKeyType, + val: "01b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + err: true, + }, { + typ: PublicKeyType, + val: "q", + err: true, + }, { + typ: StringType, + val: "q", + out: "q", + }, { + typ: StringType, + val: "dead", + out: "dead", + }, { + typ: StringType, + val: "йцукен", + out: "йцукен", + }, { + typ: ArrayType, + val: "", + err: true, + }, { + typ: MapType, + val: "[]", + err: true, + }, { + typ: InteropInterfaceType, + val: "", + err: true, + }} + + for _, inout := range inouts { + out, err := adjustValToType(inout.typ, inout.val) + if inout.err { + assert.NotNil(t, err, "should error on '%s/%s' input", inout.typ, inout.val) + } else { + assert.Nil(t, err, "shouldn't error on '%s/%s' input", inout.typ, inout.val) + assert.Equal(t, inout.out, out, "bad output for '%s/%s' input", inout.typ, inout.val) + } + } +} From 535f3915506e497284362e25f96845006dcd88f2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 27 Feb 2020 17:38:17 +0300 Subject: [PATCH 2/5] smartcontract: add marshaller for Parameter 1) Add marshaller and tests for smartcontract.Parameter 2) Add unmarshaller and tests for missing types of smartcontract.Parameter: - MapType - BoolType --- pkg/smartcontract/param_context.go | 106 +++++++++++++-- pkg/smartcontract/param_context_test.go | 163 +++++++++++++++++++++++- 2 files changed, 256 insertions(+), 13 deletions(-) diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index fe8a2561b..b1b8e3330 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -46,21 +46,94 @@ type rawParameter struct { Value json.RawMessage `json:"value"` } +type keyValuePair struct { + Key rawParameter `json:"key"` + Value rawParameter `json:"value"` +} + +type rawKeyValuePair struct { + Key json.RawMessage `json:"key"` + Value json.RawMessage `json:"value"` +} + +// MarshalJSON implements Marshaler interface. +func (p *Parameter) MarshalJSON() ([]byte, error) { + var ( + resultRawValue json.RawMessage + resultErr error + ) + switch p.Type { + case BoolType, IntegerType, StringType, Hash256Type, Hash160Type: + resultRawValue, resultErr = json.Marshal(p.Value) + case PublicKeyType, ByteArrayType, SignatureType: + resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte))) + case ArrayType: + var value = make([]rawParameter, 0) + for _, parameter := range p.Value.([]Parameter) { + rawValue, err := json.Marshal(parameter.Value) + if err != nil { + return nil, err + } + value = append(value, rawParameter{ + Type: parameter.Type, + Value: rawValue, + }) + } + resultRawValue, resultErr = json.Marshal(value) + case MapType: + var value []keyValuePair + for key, val := range p.Value.(map[Parameter]Parameter) { + rawKey, err := json.Marshal(key.Value) + if err != nil { + return nil, err + } + rawValue, err := json.Marshal(val.Value) + if err != nil { + return nil, err + } + value = append(value, keyValuePair{ + Key: rawParameter{ + Type: key.Type, + Value: rawKey, + }, + Value: rawParameter{ + Type: val.Type, + Value: rawValue, + }, + }) + } + resultRawValue, resultErr = json.Marshal(value) + default: + resultErr = errors.Errorf("Marshaller for type %s not implemented", p.Type) + } + if resultErr != nil { + return nil, resultErr + } + return json.Marshal(rawParameter{ + Type: p.Type, + Value: resultRawValue, + }) +} + // UnmarshalJSON implements Unmarshaler interface. func (p *Parameter) UnmarshalJSON(data []byte) (err error) { var ( - r rawParameter - i int64 - s string - b []byte + r rawParameter + i int64 + s string + b []byte + boolean bool ) - if err = json.Unmarshal(data, &r); err != nil { return } - switch p.Type = r.Type; r.Type { - case ByteArrayType: + case BoolType: + if err = json.Unmarshal(r.Value, &boolean); err != nil { + return + } + p.Value = boolean + case ByteArrayType, PublicKeyType: if err = json.Unmarshal(r.Value, &s); err != nil { return } @@ -93,6 +166,23 @@ func (p *Parameter) UnmarshalJSON(data []byte) (err error) { return } p.Value = rs + case MapType: + var rawMap []rawKeyValuePair + if err = json.Unmarshal(r.Value, &rawMap); err != nil { + return + } + rs := make(map[Parameter]Parameter) + for _, p := range rawMap { + var key, value Parameter + if err = json.Unmarshal(p.Key, &key); err != nil { + return + } + if err = json.Unmarshal(p.Value, &value); err != nil { + return + } + rs[key] = value + } + p.Value = rs case Hash160Type: var h util.Uint160 if err = json.Unmarshal(r.Value, &h); err != nil { @@ -106,7 +196,7 @@ func (p *Parameter) UnmarshalJSON(data []byte) (err error) { } p.Value = h default: - return errors.New("not implemented") + return errors.Errorf("Unmarshaller for type %s not implemented", p.Type) } return } diff --git a/pkg/smartcontract/param_context_test.go b/pkg/smartcontract/param_context_test.go index 288c8558c..1a188a4be 100644 --- a/pkg/smartcontract/param_context_test.go +++ b/pkg/smartcontract/param_context_test.go @@ -2,6 +2,7 @@ package smartcontract import ( "encoding/json" + "math" "reflect" "testing" @@ -9,10 +10,129 @@ import ( "github.com/stretchr/testify/assert" ) -var testCases = []struct { +var marshalJSONTestCases = []struct { + input Parameter + result string +}{ + { + input: Parameter{Type: IntegerType, Value: int64(12345)}, + result: `{"type":"Integer","value":12345}`, + }, + { + input: Parameter{Type: StringType, Value: "Some string"}, + result: `{"type":"String","value":"Some string"}`, + }, + { + input: Parameter{Type: BoolType, Value: true}, + result: `{"type":"Boolean","value":true}`, + }, + { + input: Parameter{Type: ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, + result: `{"type":"ByteArray","value":"010203"}`, + }, + { + input: Parameter{ + Type: PublicKeyType, + Value: []byte{0x03, 0xb3, 0xbf, 0x15, 0x02, 0xfb, 0xdc, 0x05, 0x44, 0x9b, 0x50, 0x6a, 0xaf, 0x04, 0x57, 0x97, 0x24, 0x02, 0x4b, 0x06, 0x54, 0x2e, 0x49, 0x26, 0x2b, 0xfa, 0xa3, 0xf7, 0x0e, 0x20, 0x00, 0x40, 0xa9}, + }, + result: `{"type":"PublicKey","value":"03b3bf1502fbdc05449b506aaf04579724024b06542e49262bfaa3f70e200040a9"}`, + }, + { + input: Parameter{ + Type: ArrayType, + Value: []Parameter{ + {Type: StringType, Value: "str 1"}, + {Type: IntegerType, Value: int64(2)}, + }, + }, + result: `{"type":"Array","value":[{"type":"String","value":"str 1"},{"type":"Integer","value":2}]}`, + }, + { + input: Parameter{ + Type: MapType, + Value: map[Parameter]Parameter{ + {Type: StringType, Value: "key1"}: {Type: IntegerType, Value: 1}, + {Type: StringType, Value: "key2"}: {Type: StringType, Value: "two"}, + }, + }, + result: `{"type":"Map","value":[{"key":{"type":"String","value":"key1"},"value":{"type":"Integer","value":1}},{"key":{"type":"String","value":"key2"},"value":{"type":"String","value":"two"}}]}`, + }, + { + input: Parameter{ + Type: MapType, + Value: map[Parameter]Parameter{ + {Type: StringType, Value: "key1"}: {Type: ArrayType, Value: []Parameter{ + {Type: StringType, Value: "str 1"}, + {Type: IntegerType, Value: int64(2)}, + }}, + }, + }, + result: `{"type":"Map","value":[{"key":{"type":"String","value":"key1"},"value":{"type":"Array","value":[{"type":"String","value":"str 1"},{"type":"Integer","value":2}]}}]}`, + }, + { + input: Parameter{ + Type: Hash160Type, + Value: util.Uint160{ + 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, + 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, + }, + }, + result: `{"type":"Hash160","value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"}`, + }, + { + input: Parameter{ + Type: Hash256Type, + Value: util.Uint256{ + 0x2d, 0xf3, 0x45, 0xf2, 0x45, 0xc5, 0x98, 0x9e, + 0x69, 0x95, 0x45, 0x06, 0xa5, 0x9e, 0x40, 0x12, + 0xc1, 0x68, 0x54, 0x48, 0x08, 0xfc, 0xcc, 0x5b, + 0x15, 0x18, 0xab, 0xa0, 0x8f, 0x30, 0x37, 0xf0, + }, + }, + result: `{"type":"Hash256","value":"0xf037308fa0ab18155bccfc08485468c112409ea5064595699e98c545f245f32d"}`, + }, +} + +var marshalJSONErrorCases = []Parameter{ + { + Type: UnknownType, + Value: nil, + }, + { + Type: InteropInterfaceType, + Value: nil, + }, + { + Type: IntegerType, + Value: math.Inf(1), + }, +} + +func TestParam_MarshalJSON(t *testing.T) { + for _, tc := range marshalJSONTestCases { + res, err := json.Marshal(&tc.input) + assert.NoError(t, err) + var actual, expected Parameter + assert.NoError(t, json.Unmarshal(res, &actual)) + assert.NoError(t, json.Unmarshal([]byte(tc.result), &expected)) + + assert.Equal(t, expected, actual) + } + + for _, input := range marshalJSONErrorCases { + _, err := json.Marshal(&input) + assert.Error(t, err) + } +} + +var unmarshalJSONTestCases = []struct { input string result Parameter }{ + { + input: `{"type":"Bool","value":true}`, + result: Parameter{Type: BoolType, Value: true}, + }, { input: `{"type":"Integer","value":12345}`, result: Parameter{Type: IntegerType, Value: int64(12345)}, @@ -63,9 +183,38 @@ var testCases = []struct { }, }, }, + { + input: `{"type":"Map","value":[{"key":{"type":"String","value":"key1"},"value":{"type":"Integer","value":1}},{"key":{"type":"String","value":"key2"},"value":{"type":"String","value":"two"}}]}`, + result: Parameter{ + Type: MapType, + Value: map[Parameter]Parameter{ + {Type: StringType, Value: "key1"}: {Type: IntegerType, Value: int64(1)}, + {Type: StringType, Value: "key2"}: {Type: StringType, Value: "two"}, + }, + }, + }, + { + input: `{"type":"Map","value":[{"key":{"type":"String","value":"key1"},"value":{"type":"Array","value":[{"type":"String","value":"str 1"},{"type":"Integer","value":2}]}}]}`, + result: Parameter{ + Type: MapType, + Value: map[Parameter]Parameter{ + {Type: StringType, Value: "key1"}: {Type: ArrayType, Value: []Parameter{ + {Type: StringType, Value: "str 1"}, + {Type: IntegerType, Value: int64(2)}, + }}, + }, + }, + }, + { + result: Parameter{ + Type: PublicKeyType, + Value: []byte{0x03, 0xb3, 0xbf, 0x15, 0x02, 0xfb, 0xdc, 0x05, 0x44, 0x9b, 0x50, 0x6a, 0xaf, 0x04, 0x57, 0x97, 0x24, 0x02, 0x4b, 0x06, 0x54, 0x2e, 0x49, 0x26, 0x2b, 0xfa, 0xa3, 0xf7, 0x0e, 0x20, 0x00, 0x40, 0xa9}, + }, + input: `{"type":"PublicKey","value":"03b3bf1502fbdc05449b506aaf04579724024b06542e49262bfaa3f70e200040a9"}`, + }, } -var errorCases = []string{ +var unmarshalJSONErrorCases = []string{ `{"type": "ByteArray","value":`, // incorrect JSON `{"type": "ByteArray","value":1}`, // incorrect Value `{"type": "ByteArray","value":"12zz"}`, // incorrect ByteArray value @@ -78,19 +227,23 @@ var errorCases = []string{ `{"type": "Hash256","value": "0bcd"}`, // incorrect Uint256 value `{"type": "Stringg","value": ""}`, // incorrect type `{"type": {},"value": ""}`, // incorrect value + `{"type": "Boolean","value": qwerty}`, // incorrect Bool value + `{"type": "Boolean","value": ""}`, // incorrect Bool value + `{"type": "Map","value": ["key": {}]}`, // incorrect Map value + `{"type": "Map","value": ["key": {"type":"String", "value":"qwer"}, "value": {"type":"Boolean"}]}`, // incorrect Map Value value + `{"type": "Map","value": ["key": {"type":"String"}, "value": {"type":"Boolean", "value":true}]}`, // incorrect Map Key value `{"type": "InteropInterface","value": ""}`, // ununmarshable type - `{"type": "Map","value": ""}`, //unmarshable type } func TestParam_UnmarshalJSON(t *testing.T) { var s Parameter - for _, tc := range testCases { + for _, tc := range unmarshalJSONTestCases { assert.NoError(t, json.Unmarshal([]byte(tc.input), &s)) assert.Equal(t, s, tc.result) } - for _, input := range errorCases { + for _, input := range unmarshalJSONErrorCases { assert.Error(t, json.Unmarshal([]byte(input), &s)) } } From b3621d4a862c00f87c256bba2b82c0544ba6ce6d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 26 Feb 2020 14:31:07 +0300 Subject: [PATCH 3/5] smartcontract: remove unused structures Removed following unused structures: -smartcontract.ContextItem -smartcontract.Signature -smartcontract.ParameterContext --- pkg/smartcontract/param_context.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index b1b8e3330..ed5e9c887 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -369,20 +369,4 @@ func NewParameterFromString(in string) (*Parameter, error) { return nil, err } return res, nil -} - -// ContextItem represents a transaction context item. -type ContextItem struct { - Script util.Uint160 - Parameters []Parameter - Signatures []Signature -} - -// Signature represents a transaction signature. -type Signature struct { - Data []byte - PublicKey []byte -} - -// ParameterContext holds the parameter context. -type ParameterContext struct{} +} \ No newline at end of file From 7d46404e2db9aa0a836bf9f55a18cb29e9252b7c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 26 Feb 2020 16:44:54 +0300 Subject: [PATCH 4/5] smartcontract: turn trigger types into Type 1) Turn trigger types from byte constants into Type 2) Add auto-generated stringer for future purposes --- pkg/core/blockchain.go | 2 +- pkg/core/interops.go | 5 ++- pkg/core/state/notification_event.go | 7 +-- pkg/smartcontract/trigger/trigger_type.go | 15 ++++--- .../trigger/trigger_type_string.go | 37 ++++++++++++++++ .../trigger/trigger_type_test.go | 43 +++++++++++++++++++ 6 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 pkg/smartcontract/trigger/trigger_type_string.go create mode 100644 pkg/smartcontract/trigger/trigger_type_test.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index a75021378..9d2ee4f8a 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1688,6 +1688,6 @@ func (bc *Blockchain) secondsPerBlock() int { return bc.config.SecondsPerBlock } -func (bc *Blockchain) newInteropContext(trigger byte, s storage.Store, block *block.Block, tx *transaction.Transaction) *interopContext { +func (bc *Blockchain) newInteropContext(trigger trigger.Type, s storage.Store, block *block.Block, tx *transaction.Transaction) *interopContext { return newInteropContext(trigger, bc, s, block, tx, bc.log) } diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 15ee9b994..8ec6e7a66 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -14,13 +14,14 @@ import ( "github.com/CityOfZion/neo-go/pkg/core/state" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/smartcontract/trigger" "github.com/CityOfZion/neo-go/pkg/vm" "go.uber.org/zap" ) type interopContext struct { bc Blockchainer - trigger byte + trigger trigger.Type block *block.Block tx *transaction.Transaction dao *cachedDao @@ -28,7 +29,7 @@ type interopContext struct { log *zap.Logger } -func newInteropContext(trigger byte, bc Blockchainer, s storage.Store, block *block.Block, tx *transaction.Transaction, log *zap.Logger) *interopContext { +func newInteropContext(trigger trigger.Type, bc Blockchainer, s storage.Store, block *block.Block, tx *transaction.Transaction, log *zap.Logger) *interopContext { dao := newCachedDao(s) nes := make([]state.NotificationEvent, 0) return &interopContext{bc, trigger, block, tx, dao, nes, log} diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index b0ea8973e..8da4a7d58 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -2,6 +2,7 @@ package state import ( "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/smartcontract/trigger" "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm" ) @@ -17,7 +18,7 @@ type NotificationEvent struct { // all resulting notifications, state, stack and other metadata. type AppExecResult struct { TxHash util.Uint256 - Trigger byte + Trigger trigger.Type VMState string GasConsumed util.Fixed8 Stack string // JSON @@ -39,7 +40,7 @@ func (ne *NotificationEvent) DecodeBinary(r *io.BinReader) { // EncodeBinary implements the Serializable interface. func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { w.WriteBytes(aer.TxHash[:]) - w.WriteB(aer.Trigger) + w.WriteB(byte(aer.Trigger)) w.WriteString(aer.VMState) aer.GasConsumed.EncodeBinary(w) w.WriteString(aer.Stack) @@ -49,7 +50,7 @@ func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { // DecodeBinary implements the Serializable interface. func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { r.ReadBytes(aer.TxHash[:]) - aer.Trigger = r.ReadB() + aer.Trigger = trigger.Type(r.ReadB()) aer.VMState = r.ReadString() aer.GasConsumed.DecodeBinary(r) aer.Stack = r.ReadString() diff --git a/pkg/smartcontract/trigger/trigger_type.go b/pkg/smartcontract/trigger/trigger_type.go index 6b5630a1e..80f499b13 100644 --- a/pkg/smartcontract/trigger/trigger_type.go +++ b/pkg/smartcontract/trigger/trigger_type.go @@ -1,13 +1,18 @@ package trigger -// Trigger typed used in C# reference node: https://github.com/neo-project/neo/blob/c64748ecbac3baeb8045b16af0d518398a6ced24/neo/SmartContract/TriggerType.cs#L3 +//go:generate stringer -type=Type + +// Type represents trigger type used in C# reference node: https://github.com/neo-project/neo/blob/c64748ecbac3baeb8045b16af0d518398a6ced24/neo/SmartContract/TriggerType.cs#L3 +type Type byte + +// Viable list of supported trigger type constants. const ( // The verification trigger indicates that the contract is being invoked as a verification function. // The verification function can accept multiple parameters, and should return a boolean value that indicates the validity of the transaction or block. // The entry point of the contract will be invoked if the contract is triggered by Verification: // main(...); // The entry point of the contract must be able to handle this type of invocation. - Verification = 0x00 + Verification Type = 0x00 // The verificationR trigger indicates that the contract is being invoked as a verification function because it is specified as a target of an output of the transaction. // The verification function accepts no parameter, and should return a boolean value that indicates the validity of the transaction. @@ -16,14 +21,14 @@ const ( // The receiving function should have the following signature: // public bool receiving() // The receiving function will be invoked automatically when a contract is receiving assets from a transfer. - VerificationR = 0x01 + VerificationR Type = 0x01 // The application trigger indicates that the contract is being invoked as an application function. // The application function can accept multiple parameters, change the states of the blockchain, and return any type of value. // The contract can have any form of entry point, but we recommend that all contracts should have the following entry point: // public byte[] main(string operation, params object[] args) // The functions can be invoked by creating an InvocationTransaction. - Application = 0x10 + Application Type = 0x10 // The ApplicationR trigger indicates that the default function received of the contract is being invoked because it is specified as a target of an output of the transaction. // The received function accepts no parameter, changes the states of the blockchain, and returns any type of value. @@ -32,5 +37,5 @@ const ( // The received function should have the following signature: // public byte[] received() // The received function will be invoked automatically when a contract is receiving assets from a transfer. - ApplicationR = 0x11 + ApplicationR Type = 0x11 ) diff --git a/pkg/smartcontract/trigger/trigger_type_string.go b/pkg/smartcontract/trigger/trigger_type_string.go new file mode 100644 index 000000000..298846f94 --- /dev/null +++ b/pkg/smartcontract/trigger/trigger_type_string.go @@ -0,0 +1,37 @@ +// Code generated by "stringer -type=Type"; DO NOT EDIT. + +package trigger + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Verification-0] + _ = x[VerificationR-1] + _ = x[Application-16] + _ = x[ApplicationR-17] +} + +const ( + _Type_name_0 = "VerificationVerificationR" + _Type_name_1 = "ApplicationApplicationR" +) + +var ( + _Type_index_0 = [...]uint8{0, 12, 25} + _Type_index_1 = [...]uint8{0, 11, 23} +) + +func (i Type) String() string { + switch { + case i <= 1: + return _Type_name_0[_Type_index_0[i]:_Type_index_0[i+1]] + case 16 <= i && i <= 17: + i -= 16 + return _Type_name_1[_Type_index_1[i]:_Type_index_1[i+1]] + default: + return "Type(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/smartcontract/trigger/trigger_type_test.go b/pkg/smartcontract/trigger/trigger_type_test.go new file mode 100644 index 000000000..211b7436d --- /dev/null +++ b/pkg/smartcontract/trigger/trigger_type_test.go @@ -0,0 +1,43 @@ +package trigger + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStringer(t *testing.T) { + tests := map[Type]string{ + Application: "Application", + ApplicationR: "ApplicationR", + Verification: "Verification", + VerificationR: "VerificationR", + } + for o, s := range tests { + assert.Equal(t, s, o.String()) + } +} + +func TestEncodeBynary(t *testing.T) { + tests := map[Type]byte{ + Verification: 0x00, + VerificationR: 0x01, + Application: 0x10, + ApplicationR: 0x11, + } + for o, b := range tests { + assert.Equal(t, b, byte(o)) + } +} + +func TestDecodeBynary(t *testing.T) { + tests := map[Type]byte{ + Verification: 0x00, + VerificationR: 0x01, + Application: 0x10, + ApplicationR: 0x11, + } + for o, b := range tests { + assert.Equal(t, o, Type(b)) + } +} From ff4384d7ffbd90ec73d5a08a6a0a530b6139536e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 21 Feb 2020 17:56:28 +0300 Subject: [PATCH 5/5] rpc: implement getapplicationlog RPC Closes #500 --- docs/rpc.md | 2 +- pkg/core/blockchain.go | 6 ++ pkg/core/blockchainer.go | 1 + pkg/network/helper_test.go | 3 + pkg/rpc/response/result/application_log.go | 60 ++++++++++++++ pkg/rpc/server/prometheus.go | 8 ++ pkg/rpc/server/server.go | 38 +++++++++ pkg/rpc/server/server_test.go | 38 +++++++++ pkg/vm/context.go | 6 ++ pkg/vm/stack_item.go | 93 +++++++++++++++++++++- pkg/vm/stack_item_test.go | 91 +++++++++++++++++++++ 11 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 pkg/rpc/response/result/application_log.go create mode 100644 pkg/vm/stack_item_test.go diff --git a/docs/rpc.md b/docs/rpc.md index 101f671ed..cf99290c4 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -36,7 +36,7 @@ which would yield the response: | Method | Implemented | | ------- | ------------| | `getaccountstate` | Yes | -| `getapplicationlog` | No (#500) | +| `getapplicationlog` | Yes | | `getassetstate` | Yes | | `getbestblockhash` | Yes | | `getblock` | Yes | diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 9d2ee4f8a..40a19b029 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -839,6 +839,12 @@ func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transactio return bc.dao.GetTransaction(hash) } +// GetAppExecResult returns application execution result by the given +// tx hash. +func (bc *Blockchain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { + return bc.dao.GetAppExecResult(hash) +} + // GetStorageItem returns an item from storage. func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem { return bc.dao.GetStorageItem(scripthash, key) diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index dae78c48b..f7a3d97da 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -32,6 +32,7 @@ type Blockchainer interface { HasTransaction(util.Uint256) bool GetAssetState(util.Uint256) *state.Asset GetAccountState(util.Uint160) *state.Account + GetAppExecResult(util.Uint256) (*state.AppExecResult, error) GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 2759c37df..d03a7d7c7 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -67,6 +67,9 @@ func (chain *testChain) Close() { func (chain testChain) HeaderHeight() uint32 { return 0 } +func (chain testChain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { + panic("TODO") +} func (chain testChain) GetBlock(hash util.Uint256) (*block.Block, error) { panic("TODO") } diff --git a/pkg/rpc/response/result/application_log.go b/pkg/rpc/response/result/application_log.go new file mode 100644 index 000000000..1788d87cd --- /dev/null +++ b/pkg/rpc/response/result/application_log.go @@ -0,0 +1,60 @@ +package result + +import ( + "encoding/json" + + "github.com/CityOfZion/neo-go/pkg/core/state" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// ApplicationLog wrapper used for the representation of the +// state.AppExecResult based on the specific tx on the RPC Server. +type ApplicationLog struct { + TxHash util.Uint256 `json:"txid"` + Executions []Execution `json:"executions"` +} + +// Execution response wrapper +type Execution struct { + Trigger string `json:"trigger"` + ScriptHash util.Uint160 `json:"contract"` + VMState string `json:"vmstate"` + GasConsumed util.Fixed8 `json:"gas_consumed"` + Stack json.RawMessage `json:"stack"` + Events []NotificationEvent `json:"notifications"` +} + +//NotificationEvent response wrapper +type NotificationEvent struct { + Contract util.Uint160 `json:"contract"` + Item smartcontract.Parameter `json:"state"` +} + +// NewApplicationLog creates a new ApplicationLog wrapper. +func NewApplicationLog(appExecRes *state.AppExecResult, scriptHash util.Uint160) ApplicationLog { + events := make([]NotificationEvent, 0, len(appExecRes.Events)) + for _, e := range appExecRes.Events { + item := e.Item.ToContractParameter() + events = append(events, NotificationEvent{ + Contract: e.ScriptHash, + Item: item, + }) + } + + triggerString := appExecRes.Trigger.String() + + executions := []Execution{{ + Trigger: triggerString, + ScriptHash: scriptHash, + VMState: appExecRes.VMState, + GasConsumed: appExecRes.GasConsumed, + Stack: json.RawMessage(appExecRes.Stack), + Events: events, + }} + + return ApplicationLog{ + TxHash: appExecRes.TxHash, + Executions: executions, + } +} diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go index ff165d3b6..2d095a322 100644 --- a/pkg/rpc/server/prometheus.go +++ b/pkg/rpc/server/prometheus.go @@ -4,6 +4,13 @@ import "github.com/prometheus/client_golang/prometheus" // Metrics used in monitoring service. var ( + getapplicationlogCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getapplicationlog rpc endpoint", + Name: "getapplicationlog_called", + Namespace: "neogo", + }, + ) getbestblockhashCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getbestblockhash rpc endpoint", @@ -143,6 +150,7 @@ var ( func init() { prometheus.MustRegister( + getapplicationlogCalled, getbestblockhashCalled, getbestblockCalled, getblockcountCalled, diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index e5b84b85a..ebee6d090 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -12,6 +12,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/state" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/network" @@ -114,6 +115,10 @@ func (s *Server) methodHandler(w http.ResponseWriter, req *request.In, reqParams Methods: switch req.Method { + case "getapplicationlog": + getapplicationlogCalled.Inc() + results, resultsErr = s.getApplicationLog(reqParams) + case "getbestblockhash": getbestblockhashCalled.Inc() results = "0x" + s.chain.CurrentBlockHash().StringLE() @@ -284,6 +289,39 @@ Methods: s.WriteResponse(req, w, results) } +// getApplicationLog returns the contract log based on the specified txid. +func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, error) { + param, ok := reqParams.Value(0) + if !ok { + return nil, response.ErrInvalidParams + } + + txHash, err := param.GetUint256() + if err != nil { + return nil, response.ErrInvalidParams + } + + appExecResult, err := s.chain.GetAppExecResult(txHash) + if err != nil { + return nil, response.NewRPCError("Unknown transaction", "", nil) + } + + tx, _, err := s.chain.GetTransaction(txHash) + if err != nil { + return nil, response.NewRPCError("Error while getting transaction", "", nil) + } + + var scriptHash util.Uint160 + switch t := tx.Data.(type) { + case *transaction.InvocationTX: + scriptHash = hash.Hash160(t.Script) + default: + return nil, response.NewRPCError("Invalid transaction type", "", nil) + } + + return result.NewApplicationLog(appExecResult, scriptHash), nil +} + func (s *Server) getStorage(ps request.Params) (interface{}, error) { param, ok := ps.Value(0) if !ok { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index b94e213e8..e1b49b123 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -40,6 +40,44 @@ type rpcTestCase struct { } var rpcTestCases = map[string][]rpcTestCase{ + "getapplicationlog": { + { + name: "positive", + params: `["d5cf936296de912aa4d051531bd8d25c7a58fb68fc7f87c8d3e6e85475187c08"]`, + result: func(e *executor) interface{} { return &result.ApplicationLog{} }, + check: func(t *testing.T, e *executor, acc interface{}) { + res, ok := acc.(*result.ApplicationLog) + + require.True(t, ok) + + expectedTxHash := util.Uint256{0x8, 0x7c, 0x18, 0x75, 0x54, 0xe8, 0xe6, 0xd3, 0xc8, 0x87, 0x7f, 0xfc, 0x68, 0xfb, 0x58, 0x7a, 0x5c, 0xd2, 0xd8, 0x1b, 0x53, 0x51, 0xd0, 0xa4, 0x2a, 0x91, 0xde, 0x96, 0x62, 0x93, 0xcf, 0xd5} + assert.Equal(t, expectedTxHash, res.TxHash) + assert.Equal(t, 1, len(res.Executions)) + assert.Equal(t, "Application", res.Executions[0].Trigger) + assert.Equal(t, "HALT", res.Executions[0].VMState) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid address", + params: `["notahash"]`, + fail: true, + }, + { + name: "invalid tx hash", + params: `["d24cc1d52b5c0216cbf3835bb5bac8ccf32639fa1ab6627ec4e2b9f33f7ec02f"]`, + fail: true, + }, + { + name: "invalid tx type", + params: `["f9adfde059810f37b3d0686d67f6b29034e0c669537df7e59b40c14a0508b9ed"]`, + fail: true, + }, + }, "getaccountstate": { { name: "positive", diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 65a01fde0..bfabd0a34 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm/opcode" ) @@ -169,6 +170,11 @@ func (c *Context) Dup() StackItem { return c } +// ToContractParameter implements StackItem interface. +func (c *Context) ToContractParameter() smartcontract.Parameter { + panic("Not implemented") +} + func (c *Context) atBreakPoint() bool { for _, n := range c.breakPoints { if n == c.ip { diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 19833ead4..6571dc837 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -8,6 +8,7 @@ import ( "math/big" "reflect" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/vm/emit" ) @@ -17,6 +18,8 @@ type StackItem interface { Value() interface{} // Dup duplicates current StackItem. Dup() StackItem + // ToContractParameter converts StackItem to smartcontract.Parameter + ToContractParameter() smartcontract.Parameter } func makeStackItem(v interface{}) StackItem { @@ -115,6 +118,19 @@ func (i *StructItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *StructItem) ToContractParameter() smartcontract.Parameter { + var value []smartcontract.Parameter + for _, stackItem := range i.value { + parameter := stackItem.ToContractParameter() + value = append(value, parameter) + } + return smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: value, + } +} + // Clone returns a Struct with all Struct fields copied by value. // Array fields are still copied by reference. func (i *StructItem) Clone() *StructItem { @@ -162,6 +178,14 @@ func (i *BigIntegerItem) Dup() StackItem { return &BigIntegerItem{n.Set(i.value)} } +// ToContractParameter implements StackItem interface. +func (i *BigIntegerItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.IntegerType, + Value: i.value.Int64(), + } +} + // MarshalJSON implements the json.Marshaler interface. func (i *BigIntegerItem) MarshalJSON() ([]byte, error) { return json.Marshal(i.value) @@ -198,6 +222,14 @@ func (i *BoolItem) Dup() StackItem { return &BoolItem{i.value} } +// ToContractParameter implements StackItem interface. +func (i *BoolItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: i.value, + } +} + // ByteArrayItem represents a byte array on the stack. type ByteArrayItem struct { value []byte @@ -231,6 +263,14 @@ func (i *ByteArrayItem) Dup() StackItem { return &ByteArrayItem{a} } +// ToContractParameter implements StackItem interface. +func (i *ByteArrayItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.ByteArrayType, + Value: i.value, + } +} + // ArrayItem represents a new ArrayItem object. type ArrayItem struct { value []StackItem @@ -263,6 +303,19 @@ func (i *ArrayItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *ArrayItem) ToContractParameter() smartcontract.Parameter { + var value []smartcontract.Parameter + for _, stackItem := range i.value { + parameter := stackItem.ToContractParameter() + value = append(value, parameter) + } + return smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: value, + } +} + // MapItem represents Map object. type MapItem struct { value map[interface{}]StackItem @@ -280,7 +333,6 @@ func (i *MapItem) Value() interface{} { return i.value } -// MarshalJSON implements the json.Marshaler interface. func (i *MapItem) String() string { return "Map" } @@ -297,6 +349,23 @@ func (i *MapItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *MapItem) ToContractParameter() smartcontract.Parameter { + value := make(map[smartcontract.Parameter]smartcontract.Parameter) + for key, val := range i.value { + pValue := val.ToContractParameter() + pKey := fromMapKey(key).ToContractParameter() + if pKey.Type == smartcontract.ByteArrayType { + pKey.Value = string(pKey.Value.([]byte)) + } + value[pKey] = pValue + } + return smartcontract.Parameter{ + Type: smartcontract.MapType, + Value: value, + } +} + // Add adds key-value pair to the map. func (i *MapItem) Add(key, value StackItem) { i.value[toMapKey(key)] = value @@ -316,6 +385,20 @@ func toMapKey(key StackItem) interface{} { } } +// fromMapKey converts map key to StackItem +func fromMapKey(key interface{}) StackItem { + switch t := key.(type) { + case bool: + return &BoolItem{value: t} + case int64: + return &BigIntegerItem{value: big.NewInt(t)} + case string: + return &ByteArrayItem{value: []byte(t)} + default: + panic("wrong key type") + } +} + // InteropItem represents interop data on the stack. type InteropItem struct { value interface{} @@ -344,6 +427,14 @@ func (i *InteropItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *InteropItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.InteropInterfaceType, + Value: nil, + } +} + // MarshalJSON implements the json.Marshaler interface. func (i *InteropItem) MarshalJSON() ([]byte, error) { return json.Marshal(i.value) diff --git a/pkg/vm/stack_item_test.go b/pkg/vm/stack_item_test.go new file mode 100644 index 000000000..af54b4110 --- /dev/null +++ b/pkg/vm/stack_item_test.go @@ -0,0 +1,91 @@ +package vm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/CityOfZion/neo-go/pkg/smartcontract" +) + +var toContractParameterTestCases = []struct { + input StackItem + result smartcontract.Parameter +}{ + { + input: NewStructItem([]StackItem{ + NewBigIntegerItem(1), + NewBoolItem(true), + }), + result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ + {Type: smartcontract.IntegerType, Value: int64(1)}, + {Type: smartcontract.BoolType, Value: true}, + }}, + }, + { + input: NewBoolItem(false), + result: smartcontract.Parameter{Type: smartcontract.BoolType, Value: false}, + }, + { + input: NewByteArrayItem([]byte{0x01, 0x02, 0x03}), + result: smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, + }, + { + input: NewArrayItem([]StackItem{NewBigIntegerItem(2), NewBoolItem(true)}), + result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ + {Type: smartcontract.IntegerType, Value: int64(2)}, + {Type: smartcontract.BoolType, Value: true}, + }}, + }, + { + input: NewInteropItem(nil), + result: smartcontract.Parameter{Type: smartcontract.InteropInterfaceType, Value: nil}, + }, + { + input: &MapItem{value: map[interface{}]StackItem{ + toMapKey(NewBigIntegerItem(1)): NewBoolItem(true), + toMapKey(NewByteArrayItem([]byte("qwerty"))): NewBigIntegerItem(3), + toMapKey(NewBoolItem(true)): NewBoolItem(false), + }}, + result: smartcontract.Parameter{ + Type: smartcontract.MapType, + Value: map[smartcontract.Parameter]smartcontract.Parameter{ + {Type: smartcontract.IntegerType, Value: int64(1)}: {Type: smartcontract.BoolType, Value: true}, + {Type: smartcontract.ByteArrayType, Value: "qwerty"}: {Type: smartcontract.IntegerType, Value: int64(3)}, + {Type: smartcontract.BoolType, Value: true}: {Type: smartcontract.BoolType, Value: false}, + }, + }, + }, +} + +func TestToContractParameter(t *testing.T) { + for _, tc := range toContractParameterTestCases { + res := tc.input.ToContractParameter() + assert.Equal(t, res, tc.result) + } +} + +var fromMapKeyTestCases = []struct { + input interface{} + result StackItem +}{ + { + input: true, + result: NewBoolItem(true), + }, + { + input: int64(4), + result: NewBigIntegerItem(4), + }, + { + input: "qwerty", + result: NewByteArrayItem([]byte("qwerty")), + }, +} + +func TestFromMapKey(t *testing.T) { + for _, tc := range fromMapKeyTestCases { + res := fromMapKey(tc.input) + assert.Equal(t, res, tc.result) + } +}