native: implement StdLib contract

This commit is contained in:
Anna Shaleva 2021-03-03 18:59:10 +03:00
parent 078870fceb
commit 2b90d4455f
5 changed files with 391 additions and 0 deletions

View file

@ -19,6 +19,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/interop/native/oracle" "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/policy"
"github.com/nspcc-dev/neo-go/pkg/interop/native/roles" "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"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util" "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(management.Hash), cs.Management.Hash.BytesBE())
require.Equal(t, []byte(notary.Hash), cs.Notary.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(crypto.Hash), cs.Crypto.Hash.BytesBE())
require.Equal(t, []byte(std.Hash), cs.Std.Hash.BytesBE())
} }
// testPrintHash is a helper for updating contract hashes. // testPrintHash is a helper for updating contract hashes.
@ -189,6 +191,18 @@ func TestNativeHelpersCompile(t *testing.T) {
{"ripemd160", []string{"[]byte{1, 2, 3}"}}, {"ripemd160", []string{"[]byte{1, 2, 3}"}},
{"verifyWithECDsa", []string{"[]byte{1, 2, 3}", pub, sig, "crypto.Secp256k1"}}, {"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) { 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.ToUpper(method[:1]) + method[1:] // ASCII only
methodUpper = strings.ReplaceAll(methodUpper, "Gas", "GAS") methodUpper = strings.ReplaceAll(methodUpper, "Gas", "GAS")
methodUpper = strings.ReplaceAll(methodUpper, "Json", "JSON")
src := fmt.Sprintf(srcTmpl, name, name, methodUpper, strings.Join(params, ",")) src := fmt.Sprintf(srcTmpl, name, name, methodUpper, strings.Join(params, ","))
v, s := vmAndCompileInterop(t, src) v, s := vmAndCompileInterop(t, src)

View file

@ -25,6 +25,7 @@ type Contracts struct {
NameService *NameService NameService *NameService
Notary *Notary Notary *Notary
Crypto *Crypto Crypto *Crypto
Std *Std
Contracts []interop.Contract Contracts []interop.Contract
// persistScript is vm script which executes "onPersist" method of every native contract. // persistScript is vm script which executes "onPersist" method of every native contract.
persistScript []byte persistScript []byte
@ -62,6 +63,10 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
cs.Management = mgmt cs.Management = mgmt
cs.Contracts = append(cs.Contracts, mgmt) cs.Contracts = append(cs.Contracts, mgmt)
s := newStd()
cs.Std = s
cs.Contracts = append(cs.Contracts, s)
c := newCrypto() c := newCrypto()
cs.Crypto = c cs.Crypto = c
cs.Contracts = append(cs.Contracts, c) cs.Contracts = append(cs.Contracts, c)

View file

@ -12,4 +12,5 @@ const (
Notary = "Notary" Notary = "Notary"
NameService = "NameService" NameService = "NameService"
CryptoLib = "CryptoLib" CryptoLib = "CryptoLib"
StdLib = "StdLib"
) )

273
pkg/core/native/std.go Normal file
View file

@ -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
}

View file

@ -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)
}