neo-go/pkg/core/native/name_service.go
Evgeniy Stratonikov cbda20aca3 core: fix native method call flags
Replace `WriteStates` with `States`.

Follow neo-project/neo#2339.
Close #1775.
Related #1725.
2021-02-25 18:07:33 +03:00

750 lines
20 KiB
Go

package native
import (
"encoding/binary"
"errors"
"math"
"math/big"
"net"
"regexp"
"sort"
"strings"
"unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// NameService represents native NameService contract.
type NameService struct {
nonfungible
NEO *NEO
}
type nameState struct {
state.NFTTokenState
// Expiration is token expiration height.
Expiration uint32
// HasAdmin is true if token has admin.
HasAdmin bool
// Admin is token admin.
Admin util.Uint160
}
// RecordType represents name record type.
type RecordType byte
// Pre-defined record types.
const (
RecordTypeA RecordType = 1
RecordTypeCNAME RecordType = 5
RecordTypeTXT RecordType = 16
RecordTypeAAAA RecordType = 28
)
const (
nameServiceID = -8
prefixRoots = 10
prefixDomainPrice = 22
prefixExpiration = 20
prefixRecord = 12
secondsInYear = 365 * 24 * 3600
// DefaultDomainPrice is the default price of register method.
DefaultDomainPrice = 10_00000000
// MinDomainNameLength is minimum domain length.
MinDomainNameLength = 3
// MaxDomainNameLength is maximum domain length.
MaxDomainNameLength = 255
)
var (
// Lookahead is not supported by Go, but it is simple `(?=.{3,255}$)`,
// so we check name length explicitly.
nameRegex = regexp.MustCompile("^([a-z0-9]{1,62}\\.)+[a-z][a-z0-9]{0,15}$")
ipv4Regex = regexp.MustCompile("^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$")
ipv6Regex = regexp.MustCompile("(?:^)(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:))$")
rootRegex = regexp.MustCompile("^[a-z][a-z0-9]{0,15}$")
)
// matchName checks if provided name is valid.
func matchName(name string) bool {
ln := len(name)
return MinDomainNameLength <= ln && ln <= MaxDomainNameLength &&
nameRegex.Match([]byte(name))
}
func newNameService() *NameService {
nf := newNonFungible(nativenames.NameService, nameServiceID, "NNS", 0)
nf.getTokenKey = func(tokenID []byte) []byte {
return append([]byte{prefixNFTToken}, hash.Hash160(tokenID).BytesBE()...)
}
nf.newTokenState = func() nftTokenState {
return new(nameState)
}
nf.onTransferred = func(tok nftTokenState) {
tok.(*nameState).HasAdmin = false
}
n := &NameService{nonfungible: *nf}
desc := newDescriptor("addRoot", smartcontract.VoidType,
manifest.NewParameter("root", smartcontract.StringType))
md := newMethodAndPrice(n.addRoot, 3000000, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("setPrice", smartcontract.VoidType,
manifest.NewParameter("price", smartcontract.IntegerType))
md = newMethodAndPrice(n.setPrice, 3000000, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("getPrice", smartcontract.IntegerType)
md = newMethodAndPrice(n.getPrice, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("isAvailable", smartcontract.BoolType,
manifest.NewParameter("name", smartcontract.StringType))
md = newMethodAndPrice(n.isAvailable, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("register", smartcontract.BoolType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = newMethodAndPrice(n.register, 1000000, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("renew", smartcontract.IntegerType,
manifest.NewParameter("name", smartcontract.StringType))
md = newMethodAndPrice(n.renew, 0, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("setAdmin", smartcontract.VoidType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("admin", smartcontract.Hash160Type))
md = newMethodAndPrice(n.setAdmin, 3000000, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("setRecord", smartcontract.VoidType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType),
manifest.NewParameter("data", smartcontract.StringType))
md = newMethodAndPrice(n.setRecord, 30000000, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("getRecord", smartcontract.StringType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType))
md = newMethodAndPrice(n.getRecord, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("deleteRecord", smartcontract.VoidType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType))
md = newMethodAndPrice(n.deleteRecord, 1000000, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("resolve", smartcontract.StringType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType))
md = newMethodAndPrice(n.resolve, 3000000, callflag.ReadStates)
n.AddMethod(md, desc)
return n
}
// Metadata implements interop.Contract interface.
func (n *NameService) Metadata() *interop.ContractMD {
return &n.ContractMD
}
// Initialize implements interop.Contract interface.
func (n *NameService) Initialize(ic *interop.Context) error {
if err := n.nonfungible.Initialize(ic); err != nil {
return err
}
if err := setIntWithKey(n.ID, ic.DAO, []byte{prefixDomainPrice}, DefaultDomainPrice); err != nil {
return err
}
roots := stringList{}
return putSerializableToDAO(n.ID, ic.DAO, []byte{prefixRoots}, &roots)
}
// OnPersist implements interop.Contract interface.
func (n *NameService) OnPersist(ic *interop.Context) error {
now := uint32(ic.Block.Timestamp/1000 + 1)
keys := []string{}
ic.DAO.Seek(n.ID, []byte{prefixExpiration}, func(k, v []byte) {
if binary.BigEndian.Uint32(k) >= now {
return
}
// Removal is done separately because of `Seek` takes storage mutex.
keys = append(keys, string(k))
})
var keysToRemove [][]byte
key := []byte{prefixExpiration}
keyRecord := []byte{prefixRecord}
for i := range keys {
key[0] = prefixExpiration
key = append(key[:1], []byte(keys[i])...)
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
return err
}
keysToRemove = keysToRemove[:0]
key[0] = prefixRecord
key = append(key[:1], keys[i][4:]...)
ic.DAO.Seek(n.ID, key, func(k, v []byte) {
keysToRemove = append(keysToRemove, k)
})
for i := range keysToRemove {
keyRecord = append(keyRecord[:0], key...)
keyRecord = append(keyRecord, keysToRemove[i]...)
err := ic.DAO.DeleteStorageItem(n.ID, keyRecord)
if err != nil {
return err
}
}
key[0] = prefixNFTToken
n.burnByKey(ic, key)
}
return nil
}
// PostPersist implements interop.Contract interface.
func (n *NameService) PostPersist(ic *interop.Context) error {
return nil
}
func (n *NameService) addRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
root := toString(args[0])
if !rootRegex.Match([]byte(root)) {
panic("invalid root")
}
n.checkCommittee(ic)
roots, _ := n.getRootsInternal(ic.DAO)
if !roots.add(root) {
panic("name already exists")
}
err := putSerializableToDAO(n.ID, ic.DAO, []byte{prefixRoots}, &roots)
if err != nil {
panic(err)
}
return stackitem.Null{}
}
var maxPrice = big.NewInt(10000_00000000)
func (n *NameService) setPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item {
price := toBigInt(args[0])
if price.Sign() <= 0 || price.Cmp(maxPrice) >= 0 {
panic("invalid price")
}
n.checkCommittee(ic)
si := &state.StorageItem{Value: bigint.ToBytes(price)}
err := ic.DAO.PutStorageItem(n.ID, []byte{prefixDomainPrice}, si)
if err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(n.getPriceInternal(ic.DAO))
}
func (n *NameService) getPriceInternal(d dao.DAO) *big.Int {
si := d.GetStorageItem(n.ID, []byte{prefixDomainPrice})
return bigint.FromBytes(si.Value)
}
func (n *NameService) parseName(item stackitem.Item) (string, []string, []byte) {
name := toName(item)
names := strings.Split(name, ".")
if len(names) != 2 {
panic("invalid name")
}
return name, names, n.getTokenKey([]byte(name))
}
func (n *NameService) isAvailable(ic *interop.Context, args []stackitem.Item) stackitem.Item {
_, names, key := n.parseName(args[0])
if ic.DAO.GetStorageItem(n.ID, key) != nil {
return stackitem.NewBool(false)
}
roots, _ := n.getRootsInternal(ic.DAO)
_, ok := roots.index(names[1])
if !ok {
panic("domain is not registered")
}
return stackitem.NewBool(true)
}
func (n *NameService) getRootsInternal(d dao.DAO) (stringList, bool) {
var sl stringList
err := getSerializableFromDAO(n.ID, d, []byte{prefixRoots}, &sl)
if err != nil {
// Roots are being stored in `Initialize()` and thus must always be present.
panic(err)
}
return sl, true
}
func (n *NameService) register(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name, names, key := n.parseName(args[0])
owner := toUint160(args[1])
if !n.checkWitness(ic, owner) {
panic("owner is not witnessed")
}
if ic.DAO.GetStorageItem(n.ID, key) != nil {
return stackitem.NewBool(false)
}
roots, _ := n.getRootsInternal(ic.DAO)
if _, ok := roots.index(names[1]); !ok {
panic("missing root")
}
if !ic.VM.AddGas(n.getPriceInternal(ic.DAO).Int64()) {
panic("insufficient gas")
}
token := &nameState{
NFTTokenState: state.NFTTokenState{
Owner: owner,
Name: name,
},
Expiration: uint32(ic.Block.Timestamp/1000 + secondsInYear),
}
n.mint(ic, token)
err := ic.DAO.PutStorageItem(n.ID,
makeExpirationKey(token.Expiration, token.ID()),
&state.StorageItem{Value: []byte{0}})
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
func (n *NameService) renew(ic *interop.Context, args []stackitem.Item) stackitem.Item {
_, _, key := n.parseName(args[0])
if !ic.VM.AddGas(n.getPriceInternal(ic.DAO).Int64()) {
panic("insufficient gas")
}
token := new(nameState)
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
keyExpiration := makeExpirationKey(token.Expiration, token.ID())
if err := ic.DAO.DeleteStorageItem(n.ID, keyExpiration); err != nil {
panic(err)
}
token.Expiration += secondsInYear
err = putSerializableToDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
binary.BigEndian.PutUint32(key[1:], token.Expiration)
si := &state.StorageItem{Value: []byte{0}}
err = ic.DAO.PutStorageItem(n.ID, key, si)
if err != nil {
panic(err)
}
bi := new(big.Int).SetUint64(uint64(token.Expiration))
return stackitem.NewBigInteger(bi)
}
func (n *NameService) setAdmin(ic *interop.Context, args []stackitem.Item) stackitem.Item {
_, _, key := n.parseName(args[0])
var admin util.Uint160
_, isNull := args[1].(stackitem.Null)
if !isNull {
admin = toUint160(args[1])
if !n.checkWitness(ic, admin) {
panic("not witnessed by admin")
}
}
token := new(nameState)
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
if !n.checkWitness(ic, token.Owner) {
panic("only owner can set admin")
}
token.HasAdmin = !isNull
token.Admin = admin
err = putSerializableToDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) checkWitness(ic *interop.Context, owner util.Uint160) bool {
ok, err := runtime.CheckHashedWitness(ic, owner)
if err != nil {
panic(err)
}
return ok
}
func (n *NameService) checkCommittee(ic *interop.Context) {
if !n.NEO.checkCommittee(ic) {
panic("not witnessed by committee")
}
}
func (n *NameService) checkAdmin(ic *interop.Context, token *nameState) bool {
if n.checkWitness(ic, token.Owner) {
return true
}
return token.HasAdmin && n.checkWitness(ic, token.Admin)
}
func (n *NameService) setRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toName(args[0])
rt := toRecordType(args[1])
data := toString(args[2])
checkName(rt, data)
domain := toDomain(name)
token, _, err := n.tokenState(ic.DAO, []byte(domain))
if err != nil {
panic(err)
}
if !n.checkAdmin(ic, token.(*nameState)) {
panic("not witnessed by admin")
}
key := makeRecordKey(domain, name, rt)
si := &state.StorageItem{Value: []byte(data)}
if err := ic.DAO.PutStorageItem(n.ID, key, si); err != nil {
panic(err)
}
return stackitem.Null{}
}
func checkName(rt RecordType, name string) {
var valid bool
switch rt {
case RecordTypeA:
// We can't rely on `len(ip) == net.IPv4len` because
// IPv4 can be parsed to mapped representation.
valid = ipv4Regex.MatchString(name) &&
net.ParseIP(name) != nil
case RecordTypeCNAME:
valid = matchName(name)
case RecordTypeTXT:
valid = utf8.RuneCountInString(name) <= 255
case RecordTypeAAAA:
valid = ipv6Regex.MatchString(name) &&
net.ParseIP(name) != nil
}
if !valid {
panic("invalid name")
}
}
func (n *NameService) getRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toName(args[0])
domain := toDomain(name)
rt := toRecordType(args[1])
key := makeRecordKey(domain, name, rt)
si := ic.DAO.GetStorageItem(n.ID, key)
if si == nil {
return stackitem.Null{}
}
return stackitem.NewByteArray(si.Value)
}
func (n *NameService) deleteRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toName(args[0])
rt := toRecordType(args[1])
domain := toDomain(name)
key := n.getTokenKey([]byte(domain))
token := new(nameState)
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
if !n.checkAdmin(ic, token) {
panic("not witnessed by admin")
}
key = makeRecordKey(domain, name, rt)
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) resolve(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toString(args[0])
rt := toRecordType(args[1])
result, ok := n.resolveInternal(ic, name, rt, 2)
if !ok {
return stackitem.Null{}
}
return stackitem.NewByteArray([]byte(result))
}
func (n *NameService) resolveInternal(ic *interop.Context, name string, t RecordType, redirect int) (string, bool) {
if redirect < 0 {
panic("invalid redirect")
}
records := n.getRecordsInternal(ic.DAO, name)
if data, ok := records[t]; ok {
return data, true
}
data, ok := records[RecordTypeCNAME]
if !ok {
return "", false
}
return n.resolveInternal(ic, data, t, redirect-1)
}
func (n *NameService) getRecordsInternal(d dao.DAO, name string) map[RecordType]string {
domain := toDomain(name)
key := makeRecordKey(domain, name, 0)
key = key[:len(key)-1]
res := make(map[RecordType]string)
d.Seek(n.ID, key, func(k, v []byte) {
rt := RecordType(k[len(k)-1])
var si state.StorageItem
r := io.NewBinReaderFromBuf(v)
si.DecodeBinary(r)
if r.Err != nil {
panic(r.Err)
}
res[rt] = string(si.Value)
})
return res
}
func makeRecordKey(domain, name string, rt RecordType) []byte {
key := make([]byte, 1+util.Uint160Size+util.Uint160Size+1)
key[0] = prefixRecord
i := 1
i += copy(key[i:], hash.Hash160([]byte(domain)).BytesBE())
i += copy(key[i:], hash.Hash160([]byte(name)).BytesBE())
key[i] = byte(rt)
return key
}
func makeExpirationKey(expiration uint32, tokenID []byte) []byte {
key := make([]byte, 1+4+util.Uint160Size)
key[0] = prefixExpiration
binary.BigEndian.PutUint32(key[1:], expiration)
copy(key[5:], hash.Hash160(tokenID).BytesBE())
return key
}
// ToMap implements nftTokenState interface.
func (s *nameState) ToMap() *stackitem.Map {
m := s.NFTTokenState.ToMap()
m.Add(stackitem.NewByteArray([]byte("expiration")),
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(s.Expiration))))
return m
}
// EncodeBinary implements io.Serializable.
func (s *nameState) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
}
// DecodeBinary implements io.Serializable.
func (s *nameState) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
s.FromStackItem(item)
}
}
// ToStackItem implements nftTokenState interface.
func (s *nameState) ToStackItem() stackitem.Item {
item := s.NFTTokenState.ToStackItem().(*stackitem.Struct)
exp := new(big.Int).SetUint64(uint64(s.Expiration))
item.Append(stackitem.NewBigInteger(exp))
if s.HasAdmin {
item.Append(stackitem.NewByteArray(s.Admin.BytesBE()))
} else {
item.Append(stackitem.Null{})
}
return item
}
// FromStackItem implements nftTokenState interface.
func (s *nameState) FromStackItem(item stackitem.Item) error {
err := s.NFTTokenState.FromStackItem(item)
if err != nil {
return err
}
elems := item.Value().([]stackitem.Item)
if len(elems) < 4 {
return errors.New("invalid stack item")
}
bi, err := elems[2].TryInteger()
if err != nil || !bi.IsUint64() {
return errors.New("invalid stack item")
}
_, isNull := elems[3].(stackitem.Null)
if !isNull {
bs, err := elems[3].TryBytes()
if err != nil {
return err
}
u, err := util.Uint160DecodeBytesBE(bs)
if err != nil {
return err
}
s.Admin = u
}
s.Expiration = uint32(bi.Uint64())
s.HasAdmin = !isNull
return nil
}
// Helpers
func domainFromString(name string) (string, bool) {
i := strings.LastIndexAny(name, ".")
if i < 0 {
return "", false
}
i = strings.LastIndexAny(name[:i], ".")
if i < 0 {
return name, true
}
return name[i+1:], true
}
func toDomain(name string) string {
domain, ok := domainFromString(name)
if !ok {
panic("invalid record")
}
return domain
}
func toRecordType(item stackitem.Item) RecordType {
bi, err := item.TryInteger()
if err != nil || !bi.IsInt64() {
panic("invalid record type")
}
val := bi.Uint64()
if val > math.MaxUint8 {
panic("invalid record type")
}
switch rt := RecordType(val); rt {
case RecordTypeA, RecordTypeCNAME, RecordTypeTXT, RecordTypeAAAA:
return rt
default:
panic("invalid record type")
}
}
func toName(item stackitem.Item) string {
name := toString(item)
if !matchName(name) {
panic("invalid name")
}
return name
}
type stringList []string
// ToStackItem converts sl to stack item.
func (sl stringList) ToStackItem() stackitem.Item {
arr := make([]stackitem.Item, len(sl))
for i := range sl {
arr[i] = stackitem.NewByteArray([]byte(sl[i]))
}
return stackitem.NewArray(arr)
}
// FromStackItem converts stack item to string list.
func (sl *stringList) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("invalid stack item")
}
res := make([]string, len(arr))
for i := range res {
s, err := stackitem.ToString(arr[i])
if err != nil {
return err
}
res[i] = s
}
*sl = res
return nil
}
// EncodeBinary implements io.Serializable.
func (sl stringList) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(sl.ToStackItem(), w)
}
// DecodeBinary implements io.Serializable.
func (sl *stringList) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
sl.FromStackItem(item)
}
}
func (sl stringList) index(s string) (int, bool) {
index := sort.Search(len(sl), func(i int) bool {
return sl[i] >= s
})
return index, index < len(sl) && sl[index] == s
}
func (sl *stringList) remove(s string) bool {
index, has := sl.index(s)
if !has {
return false
}
copy((*sl)[index:], (*sl)[index+1:])
*sl = (*sl)[:len(*sl)-1]
return true
}
func (sl *stringList) add(s string) bool {
index, has := sl.index(s)
if has {
return false
}
*sl = append(*sl, "")
copy((*sl)[index+1:], (*sl)[index:])
(*sl)[index] = s
return true
}