diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 9a52c5a97..ae99a46ea 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -19,6 +19,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop/native/oracle" "github.com/nspcc-dev/neo-go/pkg/interop/native/policy" "github.com/nspcc-dev/neo-go/pkg/interop/native/roles" + "github.com/nspcc-dev/neo-go/pkg/interop/native/std" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" @@ -39,6 +40,7 @@ func TestContractHashes(t *testing.T) { require.Equal(t, []byte(management.Hash), cs.Management.Hash.BytesBE()) require.Equal(t, []byte(notary.Hash), cs.Notary.Hash.BytesBE()) require.Equal(t, []byte(crypto.Hash), cs.Crypto.Hash.BytesBE()) + require.Equal(t, []byte(std.Hash), cs.Std.Hash.BytesBE()) } // testPrintHash is a helper for updating contract hashes. @@ -189,6 +191,18 @@ func TestNativeHelpersCompile(t *testing.T) { {"ripemd160", []string{"[]byte{1, 2, 3}"}}, {"verifyWithECDsa", []string{"[]byte{1, 2, 3}", pub, sig, "crypto.Secp256k1"}}, }) + runNativeTestCases(t, cs.Std.ContractMD, "std", []nativeTestCase{ + {"serialize", []string{"[]byte{1, 2, 3}"}}, + {"deserialize", []string{"[]byte{1, 2, 3}"}}, + {"jsonSerialize", []string{"[]byte{1, 2, 3}"}}, + {"jsonDeserialize", []string{"[]byte{1, 2, 3}"}}, + {"base64Encode", []string{"[]byte{1, 2, 3}"}}, + {"base64Decode", []string{"[]byte{1, 2, 3}"}}, + {"base58Encode", []string{"[]byte{1, 2, 3}"}}, + {"base58Decode", []string{"[]byte{1, 2, 3}"}}, + {"itoa", []string{"4", "10"}}, + {"atoi", []string{`"4"`, "10"}}, + }) } func runNativeTestCases(t *testing.T, ctr interop.ContractMD, name string, testCases []nativeTestCase) { @@ -218,6 +232,7 @@ func runNativeTestCase(t *testing.T, ctr interop.ContractMD, name, method string } methodUpper := strings.ToUpper(method[:1]) + method[1:] // ASCII only methodUpper = strings.ReplaceAll(methodUpper, "Gas", "GAS") + methodUpper = strings.ReplaceAll(methodUpper, "Json", "JSON") src := fmt.Sprintf(srcTmpl, name, name, methodUpper, strings.Join(params, ",")) v, s := vmAndCompileInterop(t, src) diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 64b8c73a9..20566f969 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -25,6 +25,7 @@ type Contracts struct { NameService *NameService Notary *Notary Crypto *Crypto + Std *Std Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -62,6 +63,10 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { cs.Management = mgmt cs.Contracts = append(cs.Contracts, mgmt) + s := newStd() + cs.Std = s + cs.Contracts = append(cs.Contracts, s) + c := newCrypto() cs.Crypto = c cs.Contracts = append(cs.Contracts, c) diff --git a/pkg/core/native/nativenames/names.go b/pkg/core/native/nativenames/names.go index 440057744..c7cbe2ca2 100644 --- a/pkg/core/native/nativenames/names.go +++ b/pkg/core/native/nativenames/names.go @@ -12,4 +12,5 @@ const ( Notary = "Notary" NameService = "NameService" CryptoLib = "CryptoLib" + StdLib = "StdLib" ) diff --git a/pkg/core/native/std.go b/pkg/core/native/std.go new file mode 100644 index 000000000..ed559000f --- /dev/null +++ b/pkg/core/native/std.go @@ -0,0 +1,273 @@ +package native + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "math/big" + "strings" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Std represents StdLib contract. +type Std struct { + interop.ContractMD +} + +const stdContractID = -2 + +var ( + // ErrInvalidBase is returned when base is invalid. + ErrInvalidBase = errors.New("invalid base") + // ErrInvalidFormat is returned when string is not a number. + ErrInvalidFormat = errors.New("invalid format") +) + +func newStd() *Std { + s := &Std{ContractMD: *interop.NewContractMD(nativenames.StdLib, stdContractID)} + defer s.UpdateHash() + + desc := newDescriptor("serialize", smartcontract.ByteArrayType, + manifest.NewParameter("item", smartcontract.AnyType)) + md := newMethodAndPrice(s.serialize, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("deserialize", smartcontract.AnyType, + manifest.NewParameter("data", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.deserialize, 1<<14, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("jsonSerialize", smartcontract.ByteArrayType, + manifest.NewParameter("item", smartcontract.AnyType)) + md = newMethodAndPrice(s.jsonSerialize, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("jsonDeserialize", smartcontract.AnyType, + manifest.NewParameter("json", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.jsonDeserialize, 1<<14, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("itoa", smartcontract.StringType, + manifest.NewParameter("value", smartcontract.IntegerType), + manifest.NewParameter("base", smartcontract.IntegerType)) + md = newMethodAndPrice(s.itoa, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("atoi", smartcontract.IntegerType, + manifest.NewParameter("value", smartcontract.StringType), + manifest.NewParameter("base", smartcontract.IntegerType)) + md = newMethodAndPrice(s.atoi, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("base64Encode", smartcontract.StringType, + manifest.NewParameter("data", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.base64Encode, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("base64Decode", smartcontract.ByteArrayType, + manifest.NewParameter("s", smartcontract.StringType)) + md = newMethodAndPrice(s.base64Decode, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("base58Encode", smartcontract.StringType, + manifest.NewParameter("data", smartcontract.ByteArrayType)) + md = newMethodAndPrice(s.base58Encode, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + desc = newDescriptor("base58Decode", smartcontract.ByteArrayType, + manifest.NewParameter("s", smartcontract.StringType)) + md = newMethodAndPrice(s.base58Decode, 1<<12, callflag.NoneFlag) + s.AddMethod(md, desc) + + return s +} + +func (s *Std) serialize(_ *interop.Context, args []stackitem.Item) stackitem.Item { + data, err := stackitem.SerializeItem(args[0]) + if err != nil { + panic(err) + } + if len(data) > stackitem.MaxSize { + panic(errors.New("too big item")) + } + + return stackitem.NewByteArray(data) +} + +func (s *Std) deserialize(_ *interop.Context, args []stackitem.Item) stackitem.Item { + data, err := args[0].TryBytes() + if err != nil { + panic(err) + } + + item, err := stackitem.DeserializeItem(data) + if err != nil { + panic(err) + } + + return item +} + +func (s *Std) jsonSerialize(_ *interop.Context, args []stackitem.Item) stackitem.Item { + data, err := stackitem.ToJSON(args[0]) + if err != nil { + panic(err) + } + if len(data) > stackitem.MaxSize { + panic(errors.New("too big item")) + } + + return stackitem.NewByteArray(data) +} + +func (s *Std) jsonDeserialize(_ *interop.Context, args []stackitem.Item) stackitem.Item { + data, err := args[0].TryBytes() + if err != nil { + panic(err) + } + + item, err := stackitem.FromJSON(data) + if err != nil { + panic(err) + } + + return item +} + +func (s *Std) itoa(_ *interop.Context, args []stackitem.Item) stackitem.Item { + num := toBigInt(args[0]) + base := toBigInt(args[1]) + if !base.IsInt64() { + panic(ErrInvalidBase) + } + var str string + switch b := base.Int64(); b { + case 10: + str = num.Text(10) + case 16: + if num.Sign() == 0 { + str = "0" + break + } + bs := bigint.ToBytes(num) + reverse(bs) + str = hex.EncodeToString(bs) + if pad := bs[0] & 0xF8; pad == 0 || pad == 0xF8 { + str = str[1:] + } + str = strings.ToUpper(str) + default: + panic(ErrInvalidBase) + } + return stackitem.NewByteArray([]byte(str)) +} + +func (s *Std) atoi(_ *interop.Context, args []stackitem.Item) stackitem.Item { + num := toString(args[0]) + base := toBigInt(args[1]) + if !base.IsInt64() { + panic(ErrInvalidBase) + } + var bi *big.Int + switch b := base.Int64(); b { + case 10: + var ok bool + bi, ok = new(big.Int).SetString(num, int(b)) + if !ok { + panic(ErrInvalidFormat) + } + case 16: + changed := len(num)%2 != 0 + if changed { + num = "0" + num + } + bs, err := hex.DecodeString(num) + if err != nil { + panic(ErrInvalidFormat) + } + if changed && bs[0]&0x8 != 0 { + bs[0] |= 0xF0 + } + reverse(bs) + bi = bigint.FromBytes(bs) + default: + panic(ErrInvalidBase) + } + + return stackitem.NewBigInteger(bi) +} + +func reverse(b []byte) { + l := len(b) + for i := 0; i < l/2; i++ { + b[i], b[l-i-1] = b[l-i-1], b[i] + } +} + +func (s *Std) base64Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item { + src, err := args[0].TryBytes() + if err != nil { + panic(err) + } + result := base64.StdEncoding.EncodeToString(src) + + return stackitem.NewByteArray([]byte(result)) +} + +func (s *Std) base64Decode(_ *interop.Context, args []stackitem.Item) stackitem.Item { + src := toString(args[0]) + result, err := base64.StdEncoding.DecodeString(src) + if err != nil { + panic(err) + } + + return stackitem.NewByteArray(result) +} + +func (s *Std) base58Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item { + src, err := args[0].TryBytes() + if err != nil { + panic(err) + } + result := base58.Encode(src) + + return stackitem.NewByteArray([]byte(result)) +} + +func (s *Std) base58Decode(_ *interop.Context, args []stackitem.Item) stackitem.Item { + src := toString(args[0]) + result, err := base58.Decode(src) + if err != nil { + panic(err) + } + + return stackitem.NewByteArray(result) +} + +// Metadata implements Contract interface. +func (s *Std) Metadata() *interop.ContractMD { + return &s.ContractMD +} + +// Initialize implements Contract interface. +func (s *Std) Initialize(ic *interop.Context) error { + return nil +} + +// OnPersist implements Contract interface. +func (s *Std) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements Contract interface. +func (s *Std) PostPersist(ic *interop.Context) error { + return nil +} diff --git a/pkg/interop/native/std/std.go b/pkg/interop/native/std/std.go new file mode 100644 index 000000000..3fcc5332a --- /dev/null +++ b/pkg/interop/native/std/std.go @@ -0,0 +1,97 @@ +package std + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" +) + +// Hash represents StdLib contract hash. +const Hash = "\xc0\xef\x39\xce\xe0\xe4\xe9\x25\xc6\xc2\xa0\x6a\x79\xe1\x44\x0d\xd8\x6f\xce\xac" + +// Serialize calls `serialize` method of StdLib native contract and serializes +// any given item into a byte slice. It works for all regular VM types (not ones +// from interop package) and allows to save them in storage or pass into Notify +// and then Deserialize them on the next run or in the external event receiver. +func Serialize(item interface{}) []byte { + return contract.Call(interop.Hash160(Hash), "serialize", contract.NoneFlag, + item).([]byte) +} + +// Deserialize calls `deserialize` method of StdLib native contract and unpacks +// previously serialized value from a byte slice, it's the opposite of Serialize. +func Deserialize(b []byte) interface{} { + return contract.Call(interop.Hash160(Hash), "deserialize", contract.NoneFlag, + b) +} + +// JSONSerialize serializes value to json. It uses `jsonSerialize` method of StdLib native +// contract. +// Serialization format is the following: +// []byte -> base64 string +// bool -> json boolean +// nil -> Null +// string -> base64 encoded sequence of underlying bytes +// (u)int* -> integer, only value in -2^53..2^53 are allowed +// []interface{} -> json array +// map[type1]type2 -> json object with string keys marshaled as strings (not base64). +func JSONSerialize(item interface{}) []byte { + return contract.Call(interop.Hash160(Hash), "jsonSerialize", contract.NoneFlag, + item).([]byte) +} + +// JSONDeserialize deserializes value from json. It uses `jsonDeserialize` method of StdLib +// native contract. +// It performs deserialization as follows: +// strings -> []byte (string) from base64 +// integers -> (u)int* types +// null -> interface{}(nil) +// arrays -> []interface{} +// maps -> map[string]interface{} +func JSONDeserialize(data []byte) interface{} { + return contract.Call(interop.Hash160(Hash), "jsonDeserialize", contract.NoneFlag, + data) +} + +// Base64Encode calls `base64Encode` method of StdLib native contract and encodes +// given byte slice into a base64 string and returns byte representation of this +// string. +func Base64Encode(b []byte) string { + return contract.Call(interop.Hash160(Hash), "base64Encode", contract.NoneFlag, + b).(string) +} + +// Base64Decode calls `base64Decode` method of StdLib native contract and decodes +// given base64 string represented as a byte slice into byte slice. +func Base64Decode(b []byte) []byte { + return contract.Call(interop.Hash160(Hash), "base64Decode", contract.NoneFlag, + b).([]byte) +} + +// Base58Encode calls `base58Encode` method of StdLib native contract and encodes +// given byte slice into a base58 string and returns byte representation of this +// string. +func Base58Encode(b []byte) string { + return contract.Call(interop.Hash160(Hash), "base58Encode", contract.NoneFlag, + b).(string) +} + +// Base58Decode calls `base58Decode` method of StdLib native contract and decodes +// given base58 string represented as a byte slice into a new byte slice. +func Base58Decode(b []byte) []byte { + return contract.Call(interop.Hash160(Hash), "base58Decode", contract.NoneFlag, + b).([]byte) +} + +// Itoa converts num in a given base to string. Base should be either 10 or 16. +// It uses `itoa` method of StdLib native contract. +func Itoa(num int, base int) string { + return contract.Call(interop.Hash160(Hash), "itoa", contract.NoneFlag, + num, base).(string) +} + +// Atoi converts string to a number in a given base. Base should be either 10 or 16. +// It uses `atoi` method of StdLib native contract. +func Atoi(s string, base int) int { + return contract.Call(interop.Hash160(Hash), "atoi", contract.NoneFlag, + s, base).(int) +}