native: add NameService

This commit is contained in:
Evgeniy Stratonikov 2021-01-22 15:12:09 +03:00
parent ec6317d643
commit e4ff8326b5
12 changed files with 1921 additions and 30 deletions

View file

@ -93,8 +93,8 @@ func newBlock(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
return newBlockWithState(cfg, index, prev, nil, txs...)
}
func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
prevState *util.Uint256, txs ...*transaction.Transaction) *block.Block {
func newBlockCustom(cfg config.ProtocolConfiguration, f func(b *block.Block),
txs ...*transaction.Transaction) *block.Block {
validators, _ := validatorsFromConfig(cfg)
valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators)
witness := transaction.Witness{
@ -103,10 +103,6 @@ func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util
b := &block.Block{
Base: block.Base{
Network: testchain.Network(),
Version: 0,
PrevHash: prev,
Timestamp: uint64(time.Now().UTC().Unix())*1000 + uint64(index),
Index: index,
NextConsensus: witness.ScriptHash(),
Script: witness,
},
@ -116,15 +112,27 @@ func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util
},
Transactions: txs,
}
if prevState != nil {
b.StateRootEnabled = true
b.PrevStateRoot = *prevState
}
f(b)
b.RebuildMerkleRoot()
b.Script.InvocationScript = testchain.Sign(b.GetSignedPart())
return b
}
func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
prevState *util.Uint256, txs ...*transaction.Transaction) *block.Block {
return newBlockCustom(cfg, func(b *block.Block) {
b.PrevHash = prev
b.Timestamp = uint64(time.Now().UTC().Unix())*1000 + uint64(index)
b.Index = index
if prevState != nil {
b.StateRootEnabled = true
b.PrevStateRoot = *prevState
}
}, txs...)
}
func (bc *Blockchain) genBlocks(n int) ([]*block.Block, error) {
blocks := make([]*block.Block, n)
lastHash := bc.topBlock.Load().(*block.Block).Hash()

View file

@ -15,14 +15,15 @@ const reservedContractID = -100
// Contracts is a set of registered native contracts.
type Contracts struct {
Management *Management
NEO *NEO
GAS *GAS
Policy *Policy
Oracle *Oracle
Designate *Designate
Notary *Notary
Contracts []interop.Contract
Management *Management
NEO *NEO
GAS *GAS
Policy *Policy
Oracle *Oracle
Designate *Designate
NameService *NameService
Notary *Notary
Contracts []interop.Contract
// persistScript is vm script which executes "onPersist" method of every native contract.
persistScript []byte
// postPersistScript is vm script which executes "postPersist" method of every native contract.
@ -87,6 +88,11 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
cs.Oracle = oracle
cs.Contracts = append(cs.Contracts, oracle)
ns := newNameService()
ns.NEO = neo
cs.NameService = ns
cs.Contracts = append(cs.Contracts, ns)
if p2pSigExtensionsEnabled {
notary := newNotary()
notary.GAS = gas

View file

@ -0,0 +1,749 @@
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 = -7
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("^(2(5[0-5]|[0-4]\\d))|1?\\d{1,2}(\\.((2(5[0-5]|[0-4]\\d))|1?\\d{1,2})){3}$")
ipv6Regex = regexp.MustCompile("^([a-f0-9A-F]{1,4}:){7}[a-f0-9A-F]{1,4}$")
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.WriteStates)
n.AddMethod(md, desc)
desc = newDescriptor("setPrice", smartcontract.VoidType,
manifest.NewParameter("price", smartcontract.IntegerType))
md = newMethodAndPrice(n.setPrice, 3000000, callflag.WriteStates)
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.WriteStates)
n.AddMethod(md, desc)
desc = newDescriptor("renew", smartcontract.IntegerType,
manifest.NewParameter("name", smartcontract.StringType))
md = newMethodAndPrice(n.renew, 0, callflag.WriteStates)
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.WriteStates)
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.WriteStates)
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.WriteStates)
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 {
si := &state.StorageItem{Value: bigint.ToBytes(big.NewInt(DefaultDomainPrice))}
if err := ic.DAO.PutStorageItem(n.ContractID, []byte{prefixDomainPrice}, si); err != nil {
return err
}
roots := stringList{}
return putSerializableToDAO(n.ContractID, 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.ContractID, []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.ContractID, key); err != nil {
return err
}
keysToRemove = keysToRemove[:0]
key[0] = prefixRecord
key = append(key[:1], keys[i][4:]...)
ic.DAO.Seek(n.ContractID, 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.ContractID, 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.ContractID, 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.ContractID, []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.ContractID, []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.ContractID, 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.ContractID, 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.ContractID, 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.ContractID,
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.ContractID, ic.DAO, key, token)
if err != nil {
panic(err)
}
keyExpiration := makeExpirationKey(token.Expiration, token.ID())
if err := ic.DAO.DeleteStorageItem(n.ContractID, keyExpiration); err != nil {
panic(err)
}
token.Expiration += secondsInYear
err = putSerializableToDAO(n.ContractID, 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.ContractID, 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.ContractID, 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.ContractID, 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])
n.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.ContractID, key, si); err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) 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.ContractID, 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.ContractID, 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.ContractID, 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.ContractID, 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) < 5 {
return errors.New("invalid stack item")
}
bi, err := elems[3].TryInteger()
if err != nil || !bi.IsUint64() {
return errors.New("invalid stack item")
}
_, isNull := elems[4].(stackitem.Null)
if !isNull {
bs, err := elems[4].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
}

View file

@ -0,0 +1,32 @@
package native
import (
"testing"
"github.com/stretchr/testify/require"
)
// The specification is following C# code:
// string domain = string.Join('.', name.Split('.')[^2..]);
func TestParseDomain(t *testing.T) {
testCases := []struct {
name string
domain string
}{
{"sim.pl.e", "pl.e"},
{"some.long.d.o.m.a.i.n", "i.n"},
{"t.wo", "t.wo"},
{".dot", ".dot"},
{".d.ot", "d.ot"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dom, ok := domainFromString(tc.name)
require.True(t, ok)
require.Equal(t, tc.domain, dom)
})
}
_, ok := domainFromString("nodots")
require.False(t, ok)
}

View file

@ -9,4 +9,5 @@ const (
Oracle = "OracleContract"
Designation = "RoleManagement"
Notary = "Notary"
NameService = "NameService"
)

View file

@ -0,0 +1,357 @@
package native
import (
"bytes"
"errors"
"math/big"
"sort"
"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"
istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"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"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
type nonfungible struct {
interop.ContractMD
tokenSymbol string
tokenDecimals byte
onTransferred func(nftTokenState)
getTokenKey func([]byte) []byte
newTokenState func() nftTokenState
}
type nftTokenState interface {
io.Serializable
ToStackItem() stackitem.Item
FromStackItem(stackitem.Item) error
ToMap() *stackitem.Map
ID() []byte
Base() *state.NFTTokenState
}
const (
prefixNFTTotalSupply = 11
prefixNFTAccount = 7
prefixNFTToken = 5
)
var (
nftTotalSupplyKey = []byte{prefixNFTTotalSupply}
intOne = big.NewInt(1)
)
func newNonFungible(name string, id int32, symbol string, decimals byte) *nonfungible {
n := &nonfungible{
ContractMD: *interop.NewContractMD(name, id),
tokenSymbol: symbol,
tokenDecimals: decimals,
getTokenKey: func(tokenID []byte) []byte {
return append([]byte{prefixNFTToken}, tokenID...)
},
newTokenState: func() nftTokenState {
return new(state.NFTTokenState)
},
}
desc := newDescriptor("symbol", smartcontract.StringType)
md := newMethodAndPrice(n.symbol, 0, callflag.NoneFlag)
n.AddMethod(md, desc)
desc = newDescriptor("decimals", smartcontract.IntegerType)
md = newMethodAndPrice(n.decimals, 0, callflag.NoneFlag)
n.AddMethod(md, desc)
desc = newDescriptor("totalSupply", smartcontract.IntegerType)
md = newMethodAndPrice(n.totalSupply, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("ownerOf", smartcontract.Hash160Type,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
md = newMethodAndPrice(n.OwnerOf, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("balanceOf", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = newMethodAndPrice(n.BalanceOf, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("properties", smartcontract.MapType,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
md = newMethodAndPrice(n.Properties, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("tokens", smartcontract.InteropInterfaceType)
md = newMethodAndPrice(n.tokens, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("tokensOf", smartcontract.InteropInterfaceType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = newMethodAndPrice(n.tokensOf, 1000000, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("transfer", smartcontract.BoolType,
manifest.NewParameter("to", smartcontract.Hash160Type),
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
md = newMethodAndPrice(n.transfer, 9000000, callflag.WriteStates|callflag.AllowNotify)
n.AddMethod(md, desc)
n.AddEvent("Transfer",
manifest.NewParameter("from", smartcontract.Hash160Type),
manifest.NewParameter("to", smartcontract.Hash160Type),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
return n
}
func (n *nonfungible) symbol(_ *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewByteArray([]byte(n.tokenSymbol))
}
func (n *nonfungible) decimals(_ *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(int64(n.tokenDecimals)))
}
func (n *nonfungible) totalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(n.TotalSupply(ic.DAO))
}
func (n *nonfungible) TotalSupply(d dao.DAO) *big.Int {
si := d.GetStorageItem(n.ContractID, nftTotalSupplyKey)
if si == nil {
return big.NewInt(0)
}
return bigint.FromBytes(si.Value)
}
func (n *nonfungible) setTotalSupply(d dao.DAO, ts *big.Int) {
si := &state.StorageItem{Value: bigint.ToBytes(ts)}
err := d.PutStorageItem(n.ContractID, nftTotalSupplyKey, si)
if err != nil {
panic(err)
}
}
func (n *nonfungible) tokenState(d dao.DAO, tokenID []byte) (nftTokenState, []byte, error) {
key := n.getTokenKey(tokenID)
s := n.newTokenState()
err := getSerializableFromDAO(n.ContractID, d, key, s)
return s, key, err
}
func (n *nonfungible) accountState(d dao.DAO, owner util.Uint160) (*state.NFTAccountState, []byte, error) {
acc := new(state.NFTAccountState)
keyAcc := makeNFTAccountKey(owner)
err := getSerializableFromDAO(n.ContractID, d, keyAcc, acc)
return acc, keyAcc, err
}
func (n *nonfungible) putAccountState(d dao.DAO, key []byte, acc *state.NFTAccountState) {
var err error
if acc.Balance.Sign() == 0 {
err = d.DeleteStorageItem(n.ContractID, key)
} else {
err = putSerializableToDAO(n.ContractID, d, key, acc)
}
if err != nil {
panic(err)
}
}
func (n *nonfungible) OwnerOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID, err := args[0].TryBytes()
if err != nil {
panic(err)
}
s, _, err := n.tokenState(ic.DAO, tokenID)
if err != nil {
panic(err)
}
return stackitem.NewByteArray(s.Base().Owner.BytesBE())
}
func (n *nonfungible) Properties(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID, err := args[0].TryBytes()
if err != nil {
panic(err)
}
s, _, err := n.tokenState(ic.DAO, tokenID)
if err != nil {
panic(err)
}
return s.ToMap()
}
func (n *nonfungible) BalanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
s, _, err := n.accountState(ic.DAO, owner)
if err != nil {
if errors.Is(err, storage.ErrKeyNotFound) {
return stackitem.NewBigInteger(big.NewInt(0))
}
panic(err)
}
return stackitem.NewBigInteger(&s.Balance)
}
func (n *nonfungible) tokens(ic *interop.Context, args []stackitem.Item) stackitem.Item {
prefix := []byte{prefixNFTToken}
siMap, err := ic.DAO.GetStorageItemsWithPrefix(n.ContractID, prefix)
if err != nil {
panic(err)
}
filteredMap := stackitem.NewMap()
for k, v := range siMap {
filteredMap.Add(stackitem.NewByteArray(append(prefix, []byte(k)...)), stackitem.NewByteArray(v.Value))
}
sort.Slice(filteredMap.Value().([]stackitem.MapElement), func(i, j int) bool {
return bytes.Compare(filteredMap.Value().([]stackitem.MapElement)[i].Key.Value().([]byte),
filteredMap.Value().([]stackitem.MapElement)[j].Key.Value().([]byte)) == -1
})
iter := istorage.NewIterator(filteredMap, istorage.FindValuesOnly|istorage.FindDeserialize|istorage.FindPick1)
return stackitem.NewInterop(iter)
}
func (n *nonfungible) tokensOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
s, _, err := n.accountState(ic.DAO, owner)
if err != nil {
panic(err)
}
arr := make([]stackitem.Item, len(s.Tokens))
for i := range arr {
arr[i] = stackitem.NewByteArray(s.Tokens[i])
}
iter, _ := vm.NewIterator(stackitem.NewArray(arr))
return iter
}
func (n *nonfungible) mint(ic *interop.Context, s nftTokenState) {
key := n.getTokenKey(s.ID())
if ic.DAO.GetStorageItem(n.ContractID, key) != nil {
panic("token is already minted")
}
if err := putSerializableToDAO(n.ContractID, ic.DAO, key, s); err != nil {
panic(err)
}
owner := s.Base().Owner
acc, keyAcc, err := n.accountState(ic.DAO, owner)
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
panic(err)
}
acc.Add(s.ID())
n.putAccountState(ic.DAO, keyAcc, acc)
ts := n.TotalSupply(ic.DAO)
ts.Add(ts, intOne)
n.setTotalSupply(ic.DAO, ts)
n.postTransfer(ic, nil, &owner, s.ID())
}
func (n *nonfungible) postTransfer(ic *interop.Context, from, to *util.Uint160, tokenID []byte) {
ne := state.NotificationEvent{
ScriptHash: n.Hash,
Name: "Transfer",
Item: stackitem.NewArray([]stackitem.Item{
addrToStackItem(from),
addrToStackItem(to),
stackitem.NewBigInteger(intOne),
stackitem.NewByteArray(tokenID),
}),
}
ic.Notifications = append(ic.Notifications, ne)
}
func (n *nonfungible) burn(ic *interop.Context, tokenID []byte) {
key := n.getTokenKey(tokenID)
n.burnByKey(ic, key)
}
func (n *nonfungible) burnByKey(ic *interop.Context, key []byte) {
token := n.newTokenState()
err := getSerializableFromDAO(n.ContractID, ic.DAO, key, token)
if err != nil {
panic(err)
}
if err := ic.DAO.DeleteStorageItem(n.ContractID, key); err != nil {
panic(err)
}
owner := token.Base().Owner
acc, keyAcc, err := n.accountState(ic.DAO, owner)
if err != nil {
panic(err)
}
id := token.ID()
acc.Remove(id)
n.putAccountState(ic.DAO, keyAcc, acc)
ts := n.TotalSupply(ic.DAO)
ts.Sub(ts, intOne)
n.setTotalSupply(ic.DAO, ts)
n.postTransfer(ic, &owner, nil, id)
}
func (n *nonfungible) transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
to := toUint160(args[0])
tokenID, err := args[1].TryBytes()
if err != nil {
panic(err)
}
token, tokenKey, err := n.tokenState(ic.DAO, tokenID)
if err != nil {
panic(err)
}
from := token.Base().Owner
ok, err := runtime.CheckHashedWitness(ic, from)
if err != nil || !ok {
return stackitem.NewBool(false)
}
if from != to {
acc, key, err := n.accountState(ic.DAO, from)
if err != nil {
panic(err)
}
acc.Remove(tokenID)
n.putAccountState(ic.DAO, key, acc)
token.Base().Owner = to
n.onTransferred(token)
err = putSerializableToDAO(n.ContractID, ic.DAO, tokenKey, token)
if err != nil {
panic(err)
}
acc, key, err = n.accountState(ic.DAO, to)
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
panic(err)
}
acc.Add(tokenID)
n.putAccountState(ic.DAO, key, acc)
}
n.postTransfer(ic, &from, &to, tokenID)
return stackitem.NewBool(true)
}
func makeNFTAccountKey(owner util.Uint160) []byte {
return append([]byte{prefixNFTAccount}, owner.BytesBE()...)
}

View file

@ -8,6 +8,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error {
@ -70,3 +71,11 @@ func makeUint160Key(prefix byte, h util.Uint160) []byte {
copy(k[1:], h.BytesBE())
return k
}
func toString(item stackitem.Item) string {
s, err := stackitem.ToString(item)
if err != nil {
panic(err)
}
return s
}

View file

@ -0,0 +1,406 @@
package core
import (
"testing"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
func TestNameService_Price(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
testGetSet(t, bc, bc.contracts.NameService.Hash, "Price",
native.DefaultDomainPrice, 1, 10000_00000000)
}
func TestNonfungible(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
acc := newAccountWithGAS(t, bc)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "symbol", "NNS")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "decimals", 0)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "totalSupply", 0)
}
func TestAddRoot(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
nsHash := bc.contracts.NameService.Hash
t.Run("invalid format", func(t *testing.T) {
testNameServiceInvoke(t, bc, "addRoot", nil, "")
})
t.Run("not signed by committee", func(t *testing.T) {
aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some")
require.NoError(t, err)
checkFAULTState(t, aer)
})
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "some")
t.Run("already exists", func(t *testing.T) {
testNameServiceInvoke(t, bc, "addRoot", nil, "some")
})
}
func TestExpiration(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
true, "first.com", acc.Contract.ScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc,
"setRecord", stackitem.Null{}, "first.com", int64(native.RecordTypeTXT), "sometext")
b1 := bc.topBlock.Load().(*block.Block)
tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, bc.contracts.NameService.Hash,
"register", acc, "second.com", acc.Contract.ScriptHash())
require.NoError(t, err)
b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b1.Index + 1
b.PrevHash = b1.Hash()
b.Timestamp = b1.Timestamp + 10000
}, tx)
require.NoError(t, bc.AddBlock(b2))
checkTxHalt(t, bc, tx.Hash())
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"isAvailable", acc, "first.com")
require.NoError(t, err)
b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b2.Index + 1
b.PrevHash = b2.Hash()
b.Timestamp = b1.Timestamp + (secondsInYear+1)*1000
}, tx)
require.NoError(t, bc.AddBlock(b3))
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(true))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"isAvailable", acc, "second.com")
require.NoError(t, err)
b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b3.Index + 1
b.PrevHash = b3.Hash()
b.Timestamp = b3.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b4))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(false))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"getRecord", acc, "first.com", int64(native.RecordTypeTXT))
require.NoError(t, err)
b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b4.Index + 1
b.PrevHash = b4.Hash()
b.Timestamp = b4.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b5))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.Null{})
}
const secondsInYear = 365 * 24 * 3600
func TestRegisterAndRenew(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
testNameServiceInvoke(t, bc, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "org")
testNameServiceInvoke(t, bc, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvoke(t, bc, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, "register", nil, "neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "neo.com\n", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, native.DefaultDomainPrice, true, "register",
nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, "balanceOf", 0, testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
false, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "isAvailable", false, "neo.com")
topBlock := bc.topBlock.Load().(*block.Block)
expectedExpiration := topBlock.Timestamp/1000 + secondsInYear
props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("description"), stackitem.Make(""))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, "properties", props, "neo.com")
testNameServiceInvoke(t, bc, "balanceOf", 1, testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) {
testNameServiceInvoke(t, bc, "properties", nil, "not.exists")
testNameServiceInvoke(t, bc, "ownerOf", nil, "not.exists")
testNameServiceInvoke(t, bc, "properties", nil, []interface{}{})
testNameServiceInvoke(t, bc, "ownerOf", nil, []interface{}{})
})
// Renew
expectedExpiration += secondsInYear
testNameServiceInvokeAux(t, bc, 100_0000_0000, true, "renew", expectedExpiration, "neo.com")
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, "properties", props, "neo.com")
}
func TestSetGetRecord(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
t.Run("set before register", func(t *testing.T) {
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(native.RecordTypeTXT), "sometext")
})
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
t.Run("invalid parameters", func(t *testing.T) {
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4")
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(native.RecordTypeA), "not.an.ip.address")
})
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(native.RecordTypeA), "1.2.3.4")
})
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA))
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA), "1.2.3.4")
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA))
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA), "1.2.3.4")
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA))
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeAAAA), "2001:0000:1F1F:0000:0000:0100:11A0:ADDF")
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME), "nspcc.ru")
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeTXT), "sometext")
// Delete record.
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(native.RecordTypeCNAME))
})
testNameServiceInvoke(t, bc, "getRecord", "nspcc.ru", "neo.com", int64(native.RecordTypeCNAME))
testNameServiceInvoke(t, bc, "deleteRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME))
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME))
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA))
}
func TestSetAdmin(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
owner := newAccountWithGAS(t, bc)
admin := newAccountWithGAS(t, bc)
guest := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, owner, "register", true,
"neo.com", owner.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
// Must be witnessed by both owner and admin.
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, []*wallet.Account{owner, admin},
"setAdmin", stackitem.Null{},
"neo.com", admin.PrivateKey().GetScriptHash())
t.Run("set and delete by admin", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(native.RecordTypeTXT), "sometext")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "deleteRecord", nil,
"neo.com", int64(native.RecordTypeTXT))
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
"neo.com", int64(native.RecordTypeTXT))
})
t.Run("set admin to null", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(native.RecordTypeTXT), "sometext")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
"neo.com", nil)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", nil,
"neo.com", int64(native.RecordTypeTXT))
})
}
func TestTransfer(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
from := newAccountWithGAS(t, bc)
to := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "register",
true, "neo.com", from.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{},
"neo.com", int64(native.RecordTypeA), "1.2.3.4")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"))
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "transfer",
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "ownerOf",
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
}
func TestTokensOf(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
acc1 := newAccountWithGAS(t, bc)
acc2 := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc1, "register",
true, "neo.com", acc1.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc2, "register",
true, "nspcc.com", acc2.PrivateKey().GetScriptHash())
testTokensOf(t, bc, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, bc, acc1, nil, util.Uint160{}.BytesBE())
}
func testTokensOf(t *testing.T, bc *Blockchain, signer *wallet.Account, result [][]byte, args ...interface{}) {
method := "tokensOf"
if len(args) == 0 {
method = "tokens"
}
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, bc.contracts.NameService.Hash, method, callflag.All, args...)
for range result {
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue)
emit.Opcodes(w.BinWriter, opcode.SWAP)
}
emit.Opcodes(w.BinWriter, opcode.DROP)
emit.Int(w.BinWriter, int64(len(result)))
emit.Opcodes(w.BinWriter, opcode.PACK)
require.NoError(t, w.Err)
script := w.Bytes()
tx := transaction.New(bc.GetConfig().Magic, script, defaultNameServiceSysfee)
tx.ValidUntilBlock = bc.BlockHeight() + 1
signTxWithAccounts(bc, tx, signer)
aers, err := persistBlock(bc, tx)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aers[0])
return
}
arr := make([]stackitem.Item, 0, len(result))
for i := len(result) - 1; i >= 0; i-- {
arr = append(arr, stackitem.Make(result[i]))
}
checkResult(t, aers[0], stackitem.NewArray(arr))
}
func TestResolve(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
true, "neo.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(native.RecordTypeA), "1.2.3.4")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(native.RecordTypeCNAME), "alias.com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
true, "alias.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"alias.com", int64(native.RecordTypeTXT), "sometxt")
testNameServiceInvoke(t, bc, "resolve", "1.2.3.4",
"neo.com", int64(native.RecordTypeA))
testNameServiceInvoke(t, bc, "resolve", "alias.com",
"neo.com", int64(native.RecordTypeCNAME))
testNameServiceInvoke(t, bc, "resolve", "sometxt",
"neo.com", int64(native.RecordTypeTXT))
testNameServiceInvoke(t, bc, "resolve", stackitem.Null{},
"neo.com", int64(native.RecordTypeAAAA))
}
const (
defaultNameServiceSysfee = 4000_0000
defaultRegisterSysfee = 10_0000_0000 + native.DefaultDomainPrice
)
func testNameServiceInvoke(t *testing.T, bc *Blockchain, method string, result interface{}, args ...interface{}) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, true, method, result, args...)
}
func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) {
if sysfee < 0 {
sysfee = defaultNameServiceSysfee
}
aer, err := invokeContractMethodGeneric(bc, sysfee, bc.contracts.NameService.Hash, method, signer, args...)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aer)
return
}
checkResult(t, aer, stackitem.Make(result))
}
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
acc, err := wallet.NewAccount()
require.NoError(t, err)
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
return acc
}

View file

@ -17,7 +17,7 @@ import (
func transferFundsToCommittee(t *testing.T, chain *Blockchain) {
transferTokenFromMultisigAccount(t, chain, testchain.CommitteeScriptHash(),
chain.contracts.GAS.Hash, 100_00000000)
chain.contracts.GAS.Hash, 1000_00000000)
}
func testGetSet(t *testing.T, chain *Blockchain, hash util.Uint160, name string, defaultValue, minValue, maxValue int64) {

View file

@ -0,0 +1,185 @@
package state
import (
"bytes"
"errors"
"math/big"
"sort"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// NFTTokenState represents state of nonfungible token.
type NFTTokenState struct {
Owner util.Uint160
Name string
Description string
}
// NFTAccountState represents state of nonfunglible account.
type NFTAccountState struct {
NEP17BalanceState
Tokens [][]byte
}
// Base returns base class.
func (s *NFTTokenState) Base() *NFTTokenState {
return s
}
// ToStackItem converts NFTTokenState to stackitem.
func (s *NFTTokenState) ToStackItem() stackitem.Item {
owner := s.Owner
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(owner.BytesBE()),
stackitem.NewByteArray([]byte(s.Name)),
stackitem.NewByteArray([]byte(s.Description)),
})
}
// EncodeBinary implements io.Serializable.
func (s *NFTTokenState) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
}
// FromStackItem converts stackitem to NFTTokenState.
func (s *NFTTokenState) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok || len(arr) < 3 {
return errors.New("invalid stack item")
}
bs, err := arr[0].TryBytes()
if err != nil {
return err
}
owner, err := util.Uint160DecodeBytesBE(bs)
if err != nil {
return err
}
name, err := stackitem.ToString(arr[1])
if err != nil {
return err
}
desc, err := stackitem.ToString(arr[2])
if err != nil {
return err
}
s.Owner = owner
s.Name = name
s.Description = desc
return nil
}
// DecodeBinary implements io.Serializable.
func (s *NFTTokenState) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
r.Err = s.FromStackItem(item)
}
}
// ToMap converts NFTTokenState to Map stackitem.
func (s *NFTTokenState) ToMap() *stackitem.Map {
return stackitem.NewMapWithValue([]stackitem.MapElement{
{
Key: stackitem.NewByteArray([]byte("name")),
Value: stackitem.NewByteArray([]byte(s.Name)),
},
{
Key: stackitem.NewByteArray([]byte("description")),
Value: stackitem.NewByteArray([]byte(s.Description)),
},
})
}
// ID returns token id.
func (s *NFTTokenState) ID() []byte {
return []byte(s.Name)
}
// ToStackItem converts NFTAccountState to stackitem.
func (s *NFTAccountState) ToStackItem() stackitem.Item {
st := s.NEP17BalanceState.toStackItem().(*stackitem.Struct)
arr := make([]stackitem.Item, len(s.Tokens))
for i := range arr {
arr[i] = stackitem.NewByteArray(s.Tokens[i])
}
st.Append(stackitem.NewArray(arr))
return st
}
// FromStackItem converts stackitem to NFTAccountState.
func (s *NFTAccountState) FromStackItem(item stackitem.Item) error {
s.NEP17BalanceState.fromStackItem(item)
arr := item.Value().([]stackitem.Item)
if len(arr) < 2 {
return errors.New("invalid stack item")
}
arr, ok := arr[1].Value().([]stackitem.Item)
if !ok {
return errors.New("invalid stack item")
}
s.Tokens = make([][]byte, len(arr))
for i := range s.Tokens {
bs, err := arr[i].TryBytes()
if err != nil {
return err
}
s.Tokens[i] = bs
}
return nil
}
// EncodeBinary implements io.Serializable.
func (s *NFTAccountState) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
}
// DecodeBinary implements io.Serializable.
func (s *NFTAccountState) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
r.Err = s.FromStackItem(item)
}
}
func (s *NFTAccountState) index(tokenID []byte) (int, bool) {
lt := len(s.Tokens)
index := sort.Search(lt, func(i int) bool {
return bytes.Compare(s.Tokens[i], tokenID) >= 0
})
return index, index < lt && bytes.Equal(s.Tokens[index], tokenID)
}
// Add adds token id to the set of account tokens
// and returns true on success.
func (s *NFTAccountState) Add(tokenID []byte) bool {
index, isPresent := s.index(tokenID)
if isPresent {
return false
}
s.Balance.Add(&s.Balance, big.NewInt(1))
s.Tokens = append(s.Tokens, []byte{})
copy(s.Tokens[index+1:], s.Tokens[index:])
s.Tokens[index] = tokenID
return true
}
// Remove removes token id to the set of account tokens
// and returns true on success.
func (s *NFTAccountState) Remove(tokenID []byte) bool {
index, isPresent := s.index(tokenID)
if !isPresent {
return false
}
s.Balance.Sub(&s.Balance, big.NewInt(1))
copy(s.Tokens[index:], s.Tokens[index+1:])
s.Tokens = s.Tokens[:len(s.Tokens)-1]
return true
}

View file

@ -0,0 +1,131 @@
package state
import (
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func newStruct(args ...interface{}) *stackitem.Struct {
arr := make([]stackitem.Item, len(args))
for i := range args {
arr[i] = stackitem.Make(args[i])
}
return stackitem.NewStruct(arr)
}
func TestNFTTokenState_Serializable(t *testing.T) {
t.Run("valid", func(t *testing.T) {
s := &NFTTokenState{
Owner: random.Uint160(),
Name: "random name",
Description: "random description",
}
id := s.ID()
actual := new(NFTTokenState)
testserdes.EncodeDecodeBinary(t, s, actual)
require.Equal(t, id, actual.ID())
})
t.Run("invalid", func(t *testing.T) {
errCases := []struct {
name string
item stackitem.Item
}{
{"invalid type", stackitem.NewByteArray([]byte{1, 2, 3})},
{"invalid owner type",
newStruct(stackitem.NewArray(nil), "name", "desc")},
{"invalid owner uint160", newStruct("123", "name", "desc")},
{"invalid name",
newStruct(random.Uint160().BytesBE(), []byte{0x80}, "desc")},
{"invalid description",
newStruct(random.Uint160().BytesBE(), "name", []byte{0x80})},
}
for _, tc := range errCases {
t.Run(tc.name, func(t *testing.T) {
w := io.NewBufBinWriter()
stackitem.EncodeBinaryStackItem(tc.item, w.BinWriter)
require.NoError(t, w.Err)
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(NFTTokenState)))
})
}
})
}
func TestNFTTokenState_ToMap(t *testing.T) {
s := &NFTTokenState{
Owner: random.Uint160(),
Name: "random name",
Description: "random description",
}
m := s.ToMap()
elems := m.Value().([]stackitem.MapElement)
i := m.Index(stackitem.Make("name"))
require.True(t, i < len(elems))
require.Equal(t, []byte("random name"), elems[i].Value.Value())
i = m.Index(stackitem.Make("description"))
require.True(t, i < len(elems))
require.Equal(t, []byte("random description"), elems[i].Value.Value())
}
func TestNFTAccountState_Serializable(t *testing.T) {
t.Run("good", func(t *testing.T) {
s := &NFTAccountState{
NEP17BalanceState: NEP17BalanceState{
Balance: *big.NewInt(10),
},
Tokens: [][]byte{
{1, 2, 3},
{3},
},
}
testserdes.EncodeDecodeBinary(t, s, new(NFTAccountState))
})
t.Run("invalid", func(t *testing.T) {
errCases := []struct {
name string
item stackitem.Item
}{
{"small size", newStruct(42)},
{"not an array", newStruct(11, stackitem.NewByteArray([]byte{}))},
{"not an array",
newStruct(11, stackitem.NewArray([]stackitem.Item{
stackitem.NewArray(nil),
}))},
}
for _, tc := range errCases {
t.Run(tc.name, func(t *testing.T) {
w := io.NewBufBinWriter()
stackitem.EncodeBinaryStackItem(tc.item, w.BinWriter)
require.NoError(t, w.Err)
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(NFTAccountState)))
})
}
})
}
func TestNFTAccountState_AddRemove(t *testing.T) {
var s NFTAccountState
require.True(t, s.Add([]byte{1, 2, 3}))
require.EqualValues(t, 1, s.Balance.Int64())
require.True(t, s.Add([]byte{1}))
require.EqualValues(t, 2, s.Balance.Int64())
require.False(t, s.Add([]byte{1, 2, 3}))
require.EqualValues(t, 2, s.Balance.Int64())
require.True(t, s.Remove([]byte{1}))
require.EqualValues(t, 1, s.Balance.Int64())
require.False(t, s.Remove([]byte{1}))
require.EqualValues(t, 1, s.Balance.Int64())
require.True(t, s.Remove([]byte{1, 2, 3}))
require.EqualValues(t, 0, s.Balance.Int64())
}

View file

@ -122,27 +122,34 @@ func IteratorValue(v *VM) error {
return nil
}
// IteratorCreate handles syscall System.Iterator.Create.
func IteratorCreate(v *VM) error {
data := v.Estack().Pop()
var item stackitem.Item
switch t := data.value.(type) {
// NewIterator creates new iterator from the provided stack item.
func NewIterator(item stackitem.Item) (stackitem.Item, error) {
switch t := item.(type) {
case *stackitem.Array, *stackitem.Struct:
item = stackitem.NewInterop(&arrayWrapper{
return stackitem.NewInterop(&arrayWrapper{
index: -1,
value: t.Value().([]stackitem.Item),
})
}), nil
case *stackitem.Map:
item = NewMapIterator(t)
return NewMapIterator(t), nil
default:
data, err := t.TryBytes()
if err != nil {
return fmt.Errorf("non-iterable type %s", t.Type())
return nil, fmt.Errorf("non-iterable type %s", t.Type())
}
item = stackitem.NewInterop(&byteArrayWrapper{
return stackitem.NewInterop(&byteArrayWrapper{
index: -1,
value: data,
})
}), nil
}
}
// IteratorCreate handles syscall System.Iterator.Create.
func IteratorCreate(v *VM) error {
data := v.Estack().Pop().Item()
item, err := NewIterator(data)
if err != nil {
return err
}
v.Estack().Push(&Element{value: item})