|
|
|
@ -1,723 +0,0 @@
|
|
|
|
|
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/native/nnsrecords"
|
|
|
|
|
"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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
nameServiceID = -10
|
|
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
defer n.UpdateHash()
|
|
|
|
|
|
|
|
|
|
desc := newDescriptor("addRoot", smartcontract.VoidType,
|
|
|
|
|
manifest.NewParameter("root", smartcontract.StringType))
|
|
|
|
|
md := newMethodAndPrice(n.addRoot, 1<<15, callflag.States)
|
|
|
|
|
n.AddMethod(md, desc)
|
|
|
|
|
|
|
|
|
|
desc = newDescriptor("setPrice", smartcontract.VoidType,
|
|
|
|
|
manifest.NewParameter("price", smartcontract.IntegerType))
|
|
|
|
|
md = newMethodAndPrice(n.setPrice, 1<<15, callflag.States)
|
|
|
|
|
n.AddMethod(md, desc)
|
|
|
|
|
|
|
|
|
|
desc = newDescriptor("getPrice", smartcontract.IntegerType)
|
|
|
|
|
md = newMethodAndPrice(n.getPrice, 1<<15, callflag.ReadStates)
|
|
|
|
|
n.AddMethod(md, desc)
|
|
|
|
|
|
|
|
|
|
desc = newDescriptor("isAvailable", smartcontract.BoolType,
|
|
|
|
|
manifest.NewParameter("name", smartcontract.StringType))
|
|
|
|
|
md = newMethodAndPrice(n.isAvailable, 1<<15, 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, 1<<15, 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, 1<<15, callflag.States)
|
|
|
|
|
md.StorageFee = 20
|
|
|
|
|
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, 1<<15, callflag.States)
|
|
|
|
|
md.StorageFee = 200
|
|
|
|
|
n.AddMethod(md, desc)
|
|
|
|
|
|
|
|
|
|
desc = newDescriptor("getRecord", smartcontract.StringType,
|
|
|
|
|
manifest.NewParameter("name", smartcontract.StringType),
|
|
|
|
|
manifest.NewParameter("type", smartcontract.IntegerType))
|
|
|
|
|
md = newMethodAndPrice(n.getRecord, 1<<15, 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, 1<<15, 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, 1<<17, 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)
|
|
|
|
|
err := ic.DAO.PutStorageItem(n.ID, []byte{prefixDomainPrice}, bigint.ToBytes(price))
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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{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)
|
|
|
|
|
err = ic.DAO.PutStorageItem(n.ID, key, state.StorageItem{0})
|
|
|
|
|
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(data)
|
|
|
|
|
if err := ic.DAO.PutStorageItem(n.ID, key, si); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return stackitem.Null{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func checkName(rt nnsrecords.Type, name string) {
|
|
|
|
|
var valid bool
|
|
|
|
|
switch rt {
|
|
|
|
|
case nnsrecords.A:
|
|
|
|
|
// 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 nnsrecords.CNAME:
|
|
|
|
|
valid = matchName(name)
|
|
|
|
|
case nnsrecords.TXT:
|
|
|
|
|
valid = utf8.RuneCountInString(name) <= 255
|
|
|
|
|
case nnsrecords.AAAA:
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 nnsrecords.Type, 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[nnsrecords.CNAME]
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
return n.resolveInternal(ic, data, t, redirect-1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *NameService) getRecordsInternal(d dao.DAO, name string) map[nnsrecords.Type]string {
|
|
|
|
|
domain := toDomain(name)
|
|
|
|
|
key := makeRecordKey(domain, name, 0)
|
|
|
|
|
key = key[:len(key)-1]
|
|
|
|
|
res := make(map[nnsrecords.Type]string)
|
|
|
|
|
d.Seek(n.ID, key, func(k, v []byte) {
|
|
|
|
|
rt := nnsrecords.Type(k[len(k)-1])
|
|
|
|
|
res[rt] = string(v)
|
|
|
|
|
})
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeRecordKey(domain, name string, rt nnsrecords.Type) []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 {
|
|
|
|
|
r.Err = 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) nnsrecords.Type {
|
|
|
|
|
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 := nnsrecords.Type(val); rt {
|
|
|
|
|
case nnsrecords.A, nnsrecords.CNAME, nnsrecords.TXT, nnsrecords.AAAA:
|
|
|
|
|
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 {
|
|
|
|
|
r.Err = 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) 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
|
|
|
|
|
}
|