Compare commits

...

16 commits

Author SHA1 Message Date
Anna Shaleva
14c9858df2 nns: use MillisecondsInSecond constant where appropriate 2022-09-16 11:21:03 +03:00
Anna Shaleva
d8e0a02a86 nns: keep isAvailable in sync with register
If conflicting records '*.domain' are present on new domain
registration, then `isAvailable` should return false for this
domain. Ref.
f25296b17a.
2022-09-16 11:20:59 +03:00
Anna Shaleva
d5b1c0e429 nns: check for conflicting records on domain registration
Port f25296b17a.
2022-09-16 11:04:58 +03:00
Anna Shaleva
db9cea5ecc nns: allow arbitrary level domains
Port
d10da892d9.
2022-09-16 11:04:55 +03:00
Anna Shaleva
ea934b8e30 nns: support SOA records
Port 7f1ec13ae5
and f4762c1b56.
2022-09-16 10:59:23 +03:00
Anna Shaleva
8790602f69 nns: ensure records with the same type are not repeated
Port https://github.com/nspcc-dev/neofs-contract/pull/170.
2022-09-09 19:36:16 +03:00
Anna Shaleva
c9050cef4b nns: allow multiple records of the same type
Except for the CNAME records. Port
6ea4573ef8
and
f4762c1b56.
2022-09-09 19:36:13 +03:00
Anna Shaleva
c296f8804c nns: add test for getAllRecords 2022-09-09 19:35:54 +03:00
Anna Shaleva
4543de0923 *: update basic test chain
Apply new NNS rules.
2022-09-08 14:19:39 +03:00
Anna Shaleva
d77b35c385 nns: add admin to properties
See 14f43ba8cf/src/NameService/NameService.cs (L69).
2022-09-08 14:19:39 +03:00
Anna Shaleva
225152f2d7 nns: allow to resolve FQDN
Port 4041924a75.
2022-09-08 14:19:39 +03:00
Anna Shaleva
baf24d1c66 nns: check domain expiration for read functions
Port 432c02a369.
2022-09-08 14:19:39 +03:00
Anna Shaleva
017a6b9bc1 nns: require admin signature for subdomain registration
Port
14fc086291.
2022-09-08 14:19:39 +03:00
Anna Shaleva
5cb2a1219c nns: replace root with TLD
Port
4b86891d57.
2022-09-08 14:19:39 +03:00
Anna Shaleva
c11481b119 nns: allow hyphen in domain names
Port https://github.com/nspcc-dev/neofs-contract/pull/183.
2022-09-08 14:19:39 +03:00
Anna Shaleva
bd3722041a nns: adjust maxDomainNameFragmentLength
Port https://github.com/nspcc-dev/neofs-contract/pull/238.
2022-09-08 14:19:39 +03:00
8 changed files with 665 additions and 243 deletions

View file

@ -47,22 +47,27 @@ const (
maxRegisterPrice = 1_0000_0000_0000 maxRegisterPrice = 1_0000_0000_0000
// maxRootLength is the maximum domain root length. // maxRootLength is the maximum domain root length.
maxRootLength = 16 maxRootLength = 16
// maxDomainNameFragmentLength is the maximum length of the domain name fragment. // maxDomainNameFragmentLength is the maximum length of the domain name fragment
maxDomainNameFragmentLength = 62 maxDomainNameFragmentLength = 63
// minDomainNameLength is minimum domain length. // minDomainNameLength is minimum domain length.
minDomainNameLength = 3 minDomainNameLength = 3
// maxDomainNameLength is maximum domain length. // maxDomainNameLength is maximum domain length.
maxDomainNameLength = 255 maxDomainNameLength = 255
// maxTXTRecordLength is the maximum length of the TXT domain record. // maxTXTRecordLength is the maximum length of the TXT domain record.
maxTXTRecordLength = 255 maxTXTRecordLength = 255
// maxRecordID is the maximum value of record ID (the upper bound for the number
// of records with the same type).
maxRecordID = 255
) )
// Other constants. // Other constants.
const ( const (
// defaultRegisterPrice is the default price for new domain registration. // defaultRegisterPrice is the default price for new domain registration.
defaultRegisterPrice = 10_0000_0000 defaultRegisterPrice = 10_0000_0000
// millisecondsInSecond is the amount of milliseconds per second.
millisecondsInSecond = 1000
// millisecondsInYear is amount of milliseconds per year. // millisecondsInYear is amount of milliseconds per year.
millisecondsInYear = 365 * 24 * 3600 * 1000 millisecondsInYear = 365 * 24 * 3600 * millisecondsInSecond
) )
// RecordState is a type that registered entities are saved to. // RecordState is a type that registered entities are saved to.
@ -70,6 +75,7 @@ type RecordState struct {
Name string Name string
Type RecordType Type RecordType
Data string Data string
ID byte
} }
// Update updates NameService contract. // Update updates NameService contract.
@ -118,6 +124,7 @@ func Properties(tokenID []byte) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"name": ns.Name, "name": ns.Name,
"expiration": ns.Expiration, "expiration": ns.Expiration,
"admin": ns.Admin,
} }
} }
@ -179,22 +186,6 @@ func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool {
return true return true
} }
// AddRoot registers new root.
func AddRoot(root string) {
checkCommittee()
if !checkFragment(root, true) {
panic("invalid root format")
}
var (
ctx = storage.GetContext()
rootKey = append([]byte{prefixRoot}, []byte(root)...)
)
if storage.Get(ctx, rootKey) != nil {
panic("root already exists")
}
storage.Put(ctx, rootKey, 0)
}
// Roots returns iterator over a set of NameService roots. // Roots returns iterator over a set of NameService roots.
func Roots() iterator.Iterator { func Roots() iterator.Iterator {
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
@ -219,31 +210,94 @@ func GetPrice() int {
// IsAvailable checks whether provided domain name is available. // IsAvailable checks whether provided domain name is available.
func IsAvailable(name string) bool { func IsAvailable(name string) bool {
fragments := splitAndCheck(name, false) fragments := splitAndCheck(name, true)
if fragments == nil { if fragments == nil {
panic("invalid domain name format") panic("invalid domain name format")
} }
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil { l := len(fragments)
panic("root not found") if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[l-1])...)) == nil {
} if l != 1 {
nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...)) panic("TLD not found")
if nsBytes == nil { }
return true return true
} }
ns := std.Deserialize(nsBytes.([]byte)).(NameState) if !parentExpired(ctx, 0, fragments) {
return runtime.GetTime() >= ns.Expiration return false
}
return len(getParentConflictingRecord(ctx, name, fragments)) == 0
}
// getPrentConflictingRecord returns record of '*.name' format if they are presented.
// These records conflict with domain name to be registered.
func getParentConflictingRecord(ctx storage.Context, name string, fragments []string) string {
parentKey := getTokenKey([]byte(name[len(fragments[0])+1:]))
parentRecKey := append([]byte{prefixRecord}, parentKey...)
it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues)
suffix := []byte(name)
for iterator.Next(it) {
r := iterator.Value(it).(RecordState)
ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name))
if ind > 0 && ind+len(suffix) == len(r.Name) {
return r.Name
}
}
return ""
}
// parentExpired returns true if any domain from fragments doesn't exist or expired.
// first denotes the deepest subdomain to check.
func parentExpired(ctx storage.Context, first int, fragments []string) bool {
now := runtime.GetTime()
last := len(fragments) - 1
name := fragments[last]
for i := last; i >= first; i-- {
if i != last {
name = fragments[i] + "." + name
}
nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...))
if nsBytes == nil {
return true
}
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if now >= ns.Expiration {
return true
}
}
return false
} }
// Register registers new domain with the specified owner and name if it's available. // Register registers new domain with the specified owner and name if it's available.
func Register(name string, owner interop.Hash160) bool { func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool {
fragments := splitAndCheck(name, false) fragments := splitAndCheck(name, true)
if fragments == nil { if fragments == nil {
panic("invalid domain name format") panic("invalid domain name format")
} }
l := len(fragments)
tldKey := append([]byte{prefixRoot}, []byte(fragments[l-1])...)
ctx := storage.GetContext() ctx := storage.GetContext()
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil { tldBytes := storage.Get(ctx, tldKey)
panic("root not found") if l == 1 {
checkCommittee()
if tldBytes != nil {
panic("TLD already exists")
}
storage.Put(ctx, tldKey, 0)
} else {
if tldBytes == nil {
panic("TLD not found")
}
if parentExpired(ctx, 1, fragments) {
panic("one of the parent domains is not registered")
}
parentKey := getTokenKey([]byte(name[len(fragments[0])+1:]))
nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...))
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
ns.checkAdmin()
if conflict := getParentConflictingRecord(ctx, name, fragments); len(conflict) != 0 {
panic("parent domain has conflicting records: " + conflict)
}
} }
if !isValid(owner) { if !isValid(owner) {
@ -275,14 +329,62 @@ func Register(name string, owner interop.Hash160) bool {
ns := NameState{ ns := NameState{
Owner: owner, Owner: owner,
Name: name, Name: name,
Expiration: runtime.GetTime() + millisecondsInYear, Expiration: runtime.GetTime() + expire*millisecondsInSecond,
} }
putNameStateWithKey(ctx, tokenKey, ns) putNameStateWithKey(ctx, tokenKey, ns)
putSoaRecord(ctx, name, email, refresh, retry, expire, ttl)
updateBalance(ctx, []byte(name), owner, +1) updateBalance(ctx, []byte(name), owner, +1)
postTransfer(oldOwner, owner, []byte(name), nil) postTransfer(oldOwner, owner, []byte(name), nil)
return true return true
} }
// UpdateSOA updates soa record.
func UpdateSOA(name, email string, refresh, retry, expire, ttl int) {
if len(name) > maxDomainNameLength {
panic("invalid domain name format")
}
ctx := storage.GetContext()
ns := getNameState(ctx, []byte(name))
ns.checkAdmin()
putSoaRecord(ctx, name, email, refresh, retry, expire, ttl)
}
// putSoaRecord stores SOA domain record.
func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expire, ttl int) {
data := name + " " + email + " " +
std.Itoa(runtime.GetTime(), 10) + " " +
std.Itoa(refresh, 10) + " " +
std.Itoa(retry, 10) + " " +
std.Itoa(expire, 10) + " " +
std.Itoa(ttl, 10)
tokenId := []byte(tokenIDFromName(ctx, name))
putRecord(ctx, tokenId, name, SOA, 0, data)
}
// updateSoaSerial updates serial of the corresponding SOA domain record.
func updateSoaSerial(ctx storage.Context, tokenId []byte) {
recordKey := getRecordKey(tokenId, string(tokenId), SOA, 0)
recBytes := storage.Get(ctx, recordKey)
if recBytes == nil {
panic("SOA record not found")
}
rec := std.Deserialize(recBytes.([]byte)).(RecordState)
split := std.StringSplitNonEmpty(rec.Data, " ")
if len(split) != 7 {
panic("corrupted SOA record format")
}
split[2] = std.Itoa(runtime.GetTime(), 10) // update serial
rec.Data = split[0] + " " + split[1] + " " +
split[2] + " " + split[3] + " " +
split[4] + " " + split[5] + " " +
split[6]
recBytes = std.Serialize(rec)
storage.Put(ctx, recordKey, recBytes)
}
// Renew increases domain expiration date. // Renew increases domain expiration date.
func Renew(name string) int { func Renew(name string) int {
if len(name) > maxDomainNameLength { if len(name) > maxDomainNameLength {
@ -313,9 +415,46 @@ func SetAdmin(name string, admin interop.Hash160) {
putNameState(ctx, ns) putNameState(ctx, ns)
} }
// SetRecord adds new record of the specified type to the provided domain. // SetRecord updates record of the specified type and ID.
func SetRecord(name string, typ RecordType, data string) { func SetRecord(name string, typ RecordType, id byte, data string) {
tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext()
tokenID := checkRecord(ctx, name, typ, data)
recordKey := getRecordKey(tokenID, name, typ, id)
recBytes := storage.Get(ctx, recordKey)
if recBytes == nil {
panic("unknown record")
}
putRecord(ctx, tokenID, name, typ, id, data)
updateSoaSerial(ctx, tokenID)
}
// AddRecord adds new record of the specified type to the provided domain.
func AddRecord(name string, typ RecordType, data string) {
ctx := storage.GetContext()
tokenID := checkRecord(ctx, name, typ, data)
recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ)
var id byte
records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
for iterator.Next(records) {
r := iterator.Value(records).(RecordState)
if r.Name == name && r.Type == typ && r.Data == data {
panic("record already exists")
}
id++
}
if id > maxRecordID {
panic("maximum number of records reached")
}
if typ == CNAME && id != 0 {
panic("multiple CNAME records")
}
putRecord(ctx, tokenID, name, typ, id, data)
updateSoaSerial(ctx, tokenID)
}
// checkRecord performs record validness check and returns token ID.
func checkRecord(ctx storage.Context, name string, typ RecordType, data string) []byte {
tokenID := []byte(tokenIDFromName(ctx, name))
var ok bool var ok bool
switch typ { switch typ {
case A: case A:
@ -332,44 +471,50 @@ func SetRecord(name string, typ RecordType, data string) {
if !ok { if !ok {
panic("invalid record data") panic("invalid record data")
} }
ctx := storage.GetContext()
ns := getNameState(ctx, tokenID) ns := getNameState(ctx, tokenID)
ns.checkAdmin() ns.checkAdmin()
putRecord(ctx, tokenID, name, typ, data) return tokenID
} }
// GetRecord returns domain record of the specified type if it exists or an empty // GetRecords returns domain records of the specified type if they exist or an empty
// string if not. // array if not.
func GetRecord(name string, typ RecordType) string { func GetRecords(name string, typ RecordType) []string {
tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
tokenID := []byte(tokenIDFromName(ctx, name))
_ = getNameState(ctx, tokenID) // ensure not expired _ = getNameState(ctx, tokenID) // ensure not expired
return getRecord(ctx, tokenID, name, typ) return getRecordsByType(ctx, tokenID, name, typ)
} }
// DeleteRecord removes domain record with the specified type. // DeleteRecords removes all domain records with the specified type.
func DeleteRecord(name string, typ RecordType) { func DeleteRecords(name string, typ RecordType) {
tokenID := []byte(tokenIDFromName(name)) if typ == SOA {
panic("forbidden to delete SOA record")
}
ctx := storage.GetContext() ctx := storage.GetContext()
tokenID := []byte(tokenIDFromName(ctx, name))
ns := getNameState(ctx, tokenID) ns := getNameState(ctx, tokenID)
ns.checkAdmin() ns.checkAdmin()
recordKey := getRecordKey(tokenID, name, typ) recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ)
storage.Delete(ctx, recordKey) records := storage.Find(ctx, recordsPrefix, storage.KeysOnly)
for iterator.Next(records) {
key := iterator.Value(records).(string)
storage.Delete(ctx, key)
}
updateSoaSerial(ctx, tokenID)
} }
// Resolve resolves given name (not more then three redirects are allowed). // Resolve resolves given name (not more than three redirects are allowed) to a set
func Resolve(name string, typ RecordType) string { // of domain records.
func Resolve(name string, typ RecordType) []string {
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
return resolve(ctx, name, typ, 2) res := []string{}
return resolve(ctx, res, name, typ, 2)
} }
// GetAllRecords returns an Iterator with RecordState items for given name. // GetAllRecords returns an Iterator with RecordState items for given name.
func GetAllRecords(name string) iterator.Iterator { func GetAllRecords(name string) iterator.Iterator {
tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired return getAllRecords(ctx, name)
recordsKey := getRecordsKey(tokenID, name)
return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues)
} }
// updateBalance updates account's balance and account's tokens. // updateBalance updates account's balance and account's tokens.
@ -425,12 +570,17 @@ func getTokenKey(tokenID []byte) []byte {
// getNameState returns domain name state by the specified tokenID. // getNameState returns domain name state by the specified tokenID.
func getNameState(ctx storage.Context, tokenID []byte) NameState { func getNameState(ctx storage.Context, tokenID []byte) NameState {
tokenKey := getTokenKey(tokenID) tokenKey := getTokenKey(tokenID)
return getNameStateWithKey(ctx, tokenKey) ns := getNameStateWithKey(ctx, tokenKey)
fragments := std.StringSplit(string(tokenID), ".")
if parentExpired(ctx, 1, fragments) {
panic("parent domain has expired")
}
return ns
} }
// getNameStateWithKey returns domain name state by the specified token key. // getNameStateWithKey returns domain name state by the specified token key.
func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
nameKey := append([]byte{prefixName}, tokenKey...) nameKey := getNameStateKey(tokenKey)
nsBytes := storage.Get(ctx, nameKey) nsBytes := storage.Get(ctx, nameKey)
if nsBytes == nil { if nsBytes == nil {
panic("token not found") panic("token not found")
@ -440,6 +590,11 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
return ns return ns
} }
// getNameStateKey returns NameState key for the provided token key.
func getNameStateKey(tokenKey []byte) []byte {
return append([]byte{prefixName}, tokenKey...)
}
// putNameState stores domain name state. // putNameState stores domain name state.
func putNameState(ctx storage.Context, ns NameState) { func putNameState(ctx storage.Context, ns NameState) {
tokenKey := getTokenKey([]byte(ns.Name)) tokenKey := getTokenKey([]byte(ns.Name))
@ -453,41 +608,53 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) {
storage.Put(ctx, nameKey, nsBytes) storage.Put(ctx, nameKey, nsBytes)
} }
// getRecord returns domain record. // getRecordsByType returns domain records of the specified type or an empty array if no records found.
func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string { func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string {
recordKey := getRecordKey(tokenId, name, typ) recordsPrefix := getRecordsByTypePrefix(tokenId, name, typ)
recBytes := storage.Get(ctx, recordKey) records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
if recBytes == nil { res := []string{} // return empty slice if no records was found.
return recBytes.(string) // A hack to actually return NULL. for iterator.Next(records) {
r := iterator.Value(records).(RecordState)
if r.Type == typ {
res = append(res, r.Data)
}
} }
record := std.Deserialize(recBytes.([]byte)).(RecordState) return res
return record.Data
} }
// putRecord stores domain record. // putRecord puts the specified record to the contract storage without any additional checks.
func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) { func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) {
recordKey := getRecordKey(tokenId, name, typ) recordKey := getRecordKey(tokenId, name, typ, id)
rs := RecordState{ rs := RecordState{
Name: name, Name: name,
Type: typ, Type: typ,
Data: record, Data: data,
ID: id,
} }
recBytes := std.Serialize(rs) recBytes := std.Serialize(rs)
storage.Put(ctx, recordKey, recBytes) storage.Put(ctx, recordKey, recBytes)
} }
// getRecordsKey returns prefix used to store domain records of different types. // getRecordKey returns key used to store domain record with the specified type and ID.
func getRecordsKey(tokenId []byte, name string) []byte { // This key always have a single corresponding value.
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) func getRecordKey(tokenId []byte, name string, typ RecordType, id byte) []byte {
return append(recordKey, getTokenKey([]byte(name))...) prefix := getRecordsByTypePrefix(tokenId, name, typ)
return append(prefix, id)
} }
// getRecordKey returns key used to store domain records. // getRecordsByTypePrefix returns prefix used to store domain records with the
func getRecordKey(tokenId []byte, name string, typ RecordType) []byte { // specified type of different IDs.
recordKey := getRecordsKey(tokenId, name) func getRecordsByTypePrefix(tokenId []byte, name string, typ RecordType) []byte {
recordKey := getRecordsPrefix(tokenId, name)
return append(recordKey, []byte{byte(typ)}...) return append(recordKey, []byte{byte(typ)}...)
} }
// getRecordsPrefix returns prefix used to store domain records of different types.
func getRecordsPrefix(tokenId []byte, name string) []byte {
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...)
return append(recordKey, getTokenKey([]byte(name))...)
}
// isValid returns true if the provided address is a valid Uint160. // isValid returns true if the provided address is a valid Uint160.
func isValid(address interop.Hash160) bool { func isValid(address interop.Hash160) bool {
return address != nil && len(address) == 20 return address != nil && len(address) == 20
@ -507,6 +674,8 @@ func checkCommittee() {
} }
// checkFragment validates root or a part of domain name. // checkFragment validates root or a part of domain name.
// 1. Root domain must start with a letter.
// 2. All other fragments must start and end in a letter or a digit.
func checkFragment(v string, isRoot bool) bool { func checkFragment(v string, isRoot bool) bool {
maxLength := maxDomainNameFragmentLength maxLength := maxDomainNameFragmentLength
if isRoot { if isRoot {
@ -525,12 +694,12 @@ func checkFragment(v string, isRoot bool) bool {
return false return false
} }
} }
for i := 1; i < len(v); i++ { for i := 1; i < len(v)-1; i++ {
if !isAlNum(v[i]) { if v[i] != '-' && !isAlNum(v[i]) {
return false return false
} }
} }
return true return isAlNum(v[len(v)-1])
} }
// isAlNum checks whether provided char is a lowercase letter or a number. // isAlNum checks whether provided char is a lowercase letter or a number.
@ -546,9 +715,6 @@ func splitAndCheck(name string, allowMultipleFragments bool) []string {
} }
fragments := std.StringSplit(name, ".") fragments := std.StringSplit(name, ".")
l = len(fragments) l = len(fragments)
if l < 2 {
return nil
}
if l > 2 && !allowMultipleFragments { if l > 2 && !allowMultipleFragments {
return nil return nil
} }
@ -671,48 +837,66 @@ func checkIPv6(data string) bool {
} }
// tokenIDFromName returns token ID (domain.root) from provided name. // tokenIDFromName returns token ID (domain.root) from provided name.
func tokenIDFromName(name string) string { func tokenIDFromName(ctx storage.Context, name string) string {
fragments := splitAndCheck(name, true) fragments := splitAndCheck(name, true)
if fragments == nil { if fragments == nil {
panic("invalid domain name format") panic("invalid domain name format")
} }
l := len(fragments) sum := 0
return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):] for i := 0; i < len(fragments)-1; i++ {
tokenKey := getTokenKey([]byte(name[sum:]))
nameKey := getNameStateKey(tokenKey)
nsBytes := storage.Get(ctx, nameKey)
if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if runtime.GetTime() < ns.Expiration {
return name[sum:]
}
}
sum += len(fragments[i]) + 1
}
return name
} }
// resolve resolves provided name using record with the specified type and given // resolve resolves provided name using record with the specified type and given
// maximum redirections constraint. // maximum redirections constraint.
func resolve(ctx storage.Context, name string, typ RecordType, redirect int) string { func resolve(ctx storage.Context, res []string, name string, typ RecordType, redirect int) []string {
if redirect < 0 { if redirect < 0 {
panic("invalid redirect") panic("invalid redirect")
} }
records := getRecords(ctx, name) if len(name) == 0 {
panic("invalid name")
}
if name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
records := getAllRecords(ctx, name)
cname := "" cname := ""
for iterator.Next(records) { for iterator.Next(records) {
r := iterator.Value(records).(struct { r := iterator.Value(records).(RecordState)
key string if r.Type == typ {
rs RecordState res = append(res, r.Data)
})
value := r.rs.Data
rTyp := r.key[len(r.key)-1]
if rTyp == byte(typ) {
return value
} }
if rTyp == byte(CNAME) { if r.Type == CNAME {
cname = value cname = r.Data
} }
} }
if cname == "" { if cname == "" || typ == CNAME {
return string([]byte(nil)) return res
} }
return resolve(ctx, cname, typ, redirect-1)
// TODO: the line below must be removed from the neofs nns:
// res = append(res, cname)
// @roman-khimov, it is done in a separate commit in neofs-contracts repo, is it OK?
return resolve(ctx, res, cname, typ, redirect-1)
} }
// getRecords returns iterator over the set of records corresponded with the // getAllRecords returns iterator over the set of records corresponded with the
// specified name. // specified name. Records returned are of different types and/or different IDs.
func getRecords(ctx storage.Context, name string) iterator.Iterator { // No keys are returned.
tokenID := []byte(tokenIDFromName(name)) func getAllRecords(ctx storage.Context, name string) iterator.Iterator {
_ = getNameState(ctx, tokenID) tokenID := []byte(tokenIDFromName(ctx, name))
recordsKey := getRecordsKey(tokenID, name) _ = getNameState(ctx, tokenID) // ensure not expired.
return storage.Find(ctx, recordsKey, storage.DeserializeValues) recordsPrefix := getRecordsPrefix(tokenID, name)
return storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
} }

View file

@ -2,7 +2,7 @@ name: "NameService"
sourceurl: https://github.com/nspcc-dev/neo-go/ sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: ["NEP-11"] supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords",
"resolve", "getAllRecords"] "resolve", "getAllRecords"]
events: events:
- name: Transfer - name: Transfer

View file

@ -1,6 +1,8 @@
package nns_test package nns_test
import ( import (
"math/big"
"strconv"
"strings" "strings"
"testing" "testing"
@ -9,18 +11,30 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func newNSClient(t *testing.T) *neotest.ContractInvoker { const (
millisecondsInYear = 365 * 24 * 3600 * 1000
maxDomainNameFragmentLength = 63
)
func newNSClient(t *testing.T, registerComTLD bool) *neotest.ContractInvoker {
bc, acc := chain.NewSingle(t) bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc) e := neotest.NewExecutor(t, bc, acc, acc)
c := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") ctr := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml")
e.DeployContract(t, c, nil) e.DeployContract(t, ctr, nil)
return e.CommitteeInvoker(c.Hash) c := e.CommitteeInvoker(ctr.Hash)
if registerComTLD {
// Set expiration big enough to pass all tests.
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl)
}
return c
} }
func TestNameService_Price(t *testing.T) { func TestNameService_Price(t *testing.T) {
@ -29,7 +43,7 @@ func TestNameService_Price(t *testing.T) {
maxPrice = int64(10000_00000000) maxPrice = int64(10000_00000000)
) )
c := newNSClient(t) c := newNSClient(t, false)
t.Run("set, not signed by committee", func(t *testing.T) { t.Run("set, not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t) acc := c.NewAccount(t)
@ -62,7 +76,7 @@ func TestNameService_Price(t *testing.T) {
} }
func TestNonfungible(t *testing.T) { func TestNonfungible(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, false)
c.Signers = []neotest.Signer{c.NewAccount(t)} c.Signers = []neotest.Signer{c.NewAccount(t)}
c.Invoke(t, "NNS", "symbol") c.Invoke(t, "NNS", "symbol")
@ -70,104 +84,109 @@ func TestNonfungible(t *testing.T) {
c.Invoke(t, 0, "totalSupply") c.Invoke(t, 0, "totalSupply")
} }
func TestAddRoot(t *testing.T) { func TestRegisterTLD(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, false)
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
t.Run("invalid format", func(t *testing.T) { t.Run("invalid format", func(t *testing.T) {
c.InvokeFail(t, "invalid root format", "addRoot", "") c.InvokeFail(t, "invalid domain name format", "register", "", c.CommitteeHash, mail, refresh, retry, expire, ttl)
}) })
t.Run("not signed by committee", func(t *testing.T) { t.Run("not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t) acc := c.NewAccount(t)
c := c.WithSigners(acc) c := c.WithSigners(acc)
c.InvokeFail(t, "not witnessed by committee", "addRoot", "some") c.InvokeFail(t, "not witnessed by committee", "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl)
}) })
c.Invoke(t, stackitem.Null{}, "addRoot", "some") c.Invoke(t, true, "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl)
t.Run("already exists", func(t *testing.T) { t.Run("already exists", func(t *testing.T) {
c.InvokeFail(t, "already exists", "addRoot", "some") c.InvokeFail(t, "TLD already exists", "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl)
}) })
} }
func TestExpiration(t *testing.T) { func TestExpiration(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, true)
e := c.Executor e := c.Executor
bc := e.Chain bc := e.Chain
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t) acc := e.NewAccount(t)
cAcc := c.WithSigners(acc) cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee) // acc + committee signers for ".com"'s subdomains registration
c.Invoke(t, stackitem.Null{}, "addRoot", "com") cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "addRecord", "first.com", int64(nns.TXT), "sometext")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext")
b1 := e.TopBlock(t) b1 := e.TopBlock(t)
tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
b2 := e.NewUnsignedBlock(t, tx) b2 := e.NewUnsignedBlock(t, tx)
b2.Index = b1.Index + 1 b2.Index = b1.Index + 1
b2.PrevHash = b1.Hash() b2.PrevHash = b1.Hash()
b2.Timestamp = b1.Timestamp + 10000 b2.Timestamp = b1.Timestamp + 10000
require.NoError(t, bc.AddBlock(e.SignBlock(b2))) require.NoError(t, bc.AddBlock(e.SignBlock(b2)))
e.CheckHalt(t, tx.Hash())
tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com")
b3 := e.NewUnsignedBlock(t, tx)
b3.Index = b2.Index + 1
b3.PrevHash = b2.Hash()
b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1)
require.NoError(t, bc.AddBlock(e.SignBlock(b3)))
e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true))
tx = cAcc.PrepareInvoke(t, "isAvailable", "second.com") b3 := e.NewUnsignedBlock(t)
b4 := e.NewUnsignedBlock(t, tx) b3.Index = b2.Index + 1
b4.Index = b3.Index + 1 b3.PrevHash = b2.Hash()
b4.PrevHash = b3.Hash() b3.Timestamp = b1.Timestamp + (uint64(expire) * 1000)
b4.Timestamp = b3.Timestamp + 1000 require.NoError(t, bc.AddBlock(e.SignBlock(b3)))
require.NoError(t, bc.AddBlock(e.SignBlock(b4)))
e.CheckHalt(t, tx.Hash(), stackitem.NewBool(false))
tx = cAcc.PrepareInvoke(t, "getRecord", "first.com", int64(nns.TXT)) cAcc.Invoke(t, true, "isAvailable", "first.com") // "first.com" has been expired
b5 := e.NewUnsignedBlock(t, tx) cAcc.Invoke(t, true, "isAvailable", "second.com") // TLD "com" has been expired
b5.Index = b4.Index + 1 cAcc.InvokeFail(t, "name has expired", "getRecords", "first.com", int64(nns.TXT))
b5.PrevHash = b4.Hash()
b5.Timestamp = b4.Timestamp + 1000
require.NoError(t, bc.AddBlock(e.SignBlock(b5)))
e.CheckFault(t, tx.Hash(), "name has expired")
cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) // Re-register. // TODO: According to the new code, we can't re-register expired "com" TLD, because it's already registered; at the
cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT)) // same time we can't renew it because it's already expired. We likely need to change this logic in the contract and
// after that uncomment the lines below.
// c.Invoke(t, true, "renew", "com")
// cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) // Re-register.
// cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT))
} }
const millisecondsInYear = 365 * 24 * 3600 * 1000
func TestRegisterAndRenew(t *testing.T) { func TestRegisterAndRenew(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, false)
e := c.Executor e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
c.InvokeFail(t, "root not found", "isAvailable", "neo.com") c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com")
c.Invoke(t, stackitem.Null{}, "addRoot", "org") c.Invoke(t, true, "register", "org", c.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "root not found", "isAvailable", "neo.com") c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com")
c.Invoke(t, stackitem.Null{}, "addRoot", "com") c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl)
c.Invoke(t, true, "isAvailable", "neo.com") c.Invoke(t, true, "isAvailable", "neo.com")
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash) c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
var maxLenFragment string
for i := 0; i < maxDomainNameFragmentLength; i++ {
maxLenFragment += "q"
}
c.Invoke(t, true, "isAvailable", maxLenFragment+".com")
c.Invoke(t, true, "register", maxLenFragment+".com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.Invoke(t, true, "isAvailable", "neo.com") c.Invoke(t, true, "isAvailable", "neo.com")
c.Invoke(t, 0, "balanceOf", e.CommitteeHash) c.Invoke(t, 3, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
topBlock := e.TopBlock(t) topBlock := e.TopBlock(t)
expectedExpiration := topBlock.Timestamp + millisecondsInYear expectedExpiration := topBlock.Timestamp + uint64(expire*1000)
c.Invoke(t, false, "register", "neo.com", e.CommitteeHash) c.Invoke(t, false, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.Invoke(t, false, "isAvailable", "neo.com") c.Invoke(t, false, "isAvailable", "neo.com")
t.Run("domain names with hyphen", func(t *testing.T) {
c.InvokeFail(t, "invalid domain name format", "register", "-testdomain.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "testdomain-.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.Invoke(t, true, "register", "test-domain.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
})
props := stackitem.NewMap() props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
props.Add(stackitem.Make("admin"), stackitem.Null{}) // no admin was set
c.Invoke(t, props, "properties", "neo.com") c.Invoke(t, props, "properties", "neo.com")
c.Invoke(t, 1, "balanceOf", e.CommitteeHash) c.Invoke(t, 5, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com, neo.com, test-domain.com
c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) { t.Run("invalid token ID", func(t *testing.T) {
@ -185,42 +204,59 @@ func TestRegisterAndRenew(t *testing.T) {
c.Invoke(t, props, "properties", "neo.com") c.Invoke(t, props, "properties", "neo.com")
} }
func TestSetGetRecord(t *testing.T) { func TestSetAddGetRecord(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, true)
e := c.Executor e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t) acc := e.NewAccount(t)
cAcc := c.WithSigners(acc) cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
t.Run("set before register", func(t *testing.T) { t.Run("set before register", func(t *testing.T) {
c.InvokeFail(t, "token not found", "setRecord", "neo.com", int64(nns.TXT), "sometext") c.InvokeFail(t, "token not found", "addRecord", "neo.com", int64(nns.TXT), "sometext")
}) })
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
t.Run("invalid parameters", func(t *testing.T) { t.Run("invalid parameters", func(t *testing.T) {
c.InvokeFail(t, "unsupported record type", "setRecord", "neo.com", int64(0xFF), "1.2.3.4") c.InvokeFail(t, "unsupported record type", "addRecord", "neo.com", int64(0xFF), "1.2.3.4")
c.InvokeFail(t, "invalid record", "setRecord", "neo.com", int64(nns.A), "not.an.ip.address") c.InvokeFail(t, "invalid record", "addRecord", "neo.com", int64(nns.A), "not.an.ip.address")
}) })
t.Run("invalid witness", func(t *testing.T) { t.Run("invalid witness", func(t *testing.T) {
cAcc.InvokeFail(t, "not witnessed by admin", "setRecord", "neo.com", int64(nns.A), "1.2.3.4") cAcc.InvokeFail(t, "not witnessed by admin", "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
}) })
c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") c.InvokeFail(t, "record already exists", "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record.
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df")
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "nspcc.ru")
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext")
// Add multiple records and update some of them.
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext1")
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext2")
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext3")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{
stackitem.Make("sometext"),
stackitem.Make("sometext1"),
stackitem.Make("sometext2"),
stackitem.Make("sometext3"),
}), "getRecords", "neo.com", int64(nns.TXT))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 2, "sometext22")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{
stackitem.Make("sometext"),
stackitem.Make("sometext1"),
stackitem.Make("sometext22"),
stackitem.Make("sometext3"),
}), "getRecords", "neo.com", int64(nns.TXT))
// Delete record. // Delete record.
t.Run("invalid witness", func(t *testing.T) { t.Run("invalid witness", func(t *testing.T) {
cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME)) cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.CNAME))
}) })
c.Invoke(t, "nspcc.ru", "getRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
t.Run("SetRecord_compatibility", func(t *testing.T) { t.Run("SetRecord_compatibility", func(t *testing.T) {
// tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior
@ -280,9 +316,10 @@ func TestSetGetRecord(t *testing.T) {
args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name} args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name}
t.Run(testCase.Name, func(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) {
if testCase.ShouldFail { if testCase.ShouldFail {
c.InvokeFail(t, "", "setRecord", args...) c.InvokeFail(t, "", "addRecord", args...)
} else { } else {
c.Invoke(t, stackitem.Null{}, "setRecord", args...) c.Invoke(t, stackitem.Null{}, "addRecord", args...)
c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(testCase.Type)) // clear records after test to avoid duplicating records.
} }
}) })
} }
@ -290,19 +327,21 @@ func TestSetGetRecord(t *testing.T) {
} }
func TestSetAdmin(t *testing.T) { func TestSetAdmin(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, true)
e := c.Executor e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
owner := e.NewAccount(t) owner := e.NewAccount(t)
cOwner := c.WithSigners(owner) cOwner := c.WithSigners(owner)
cOwnerCommittee := c.WithSigners(owner, c.Committee)
admin := e.NewAccount(t) admin := e.NewAccount(t)
cAdmin := c.WithSigners(admin) cAdmin := c.WithSigners(admin)
guest := e.NewAccount(t) guest := e.NewAccount(t)
cGuest := c.WithSigners(guest) cGuest := c.WithSigners(guest)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") cOwner.InvokeFail(t, "not witnessed by admin", "register", "neo.com", owner.ScriptHash(), mail, refresh, retry, expire, ttl) // admin is committee
cOwnerCommittee.Invoke(t, true, "register", "neo.com", owner.ScriptHash(), mail, refresh, retry, expire, ttl)
cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash()) expectedExpiration := e.TopBlock(t).Timestamp + uint64(expire)*1000
cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash())
// Must be witnessed by both owner and admin. // Must be witnessed by both owner and admin.
@ -310,36 +349,42 @@ func TestSetAdmin(t *testing.T) {
cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash()) cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash())
cc := c.WithSigners(owner, admin) cc := c.WithSigners(owner, admin)
cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash())
props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
props.Add(stackitem.Make("admin"), stackitem.Make(admin.ScriptHash().BytesBE()))
c.Invoke(t, props, "properties", "neo.com")
t.Run("set and delete by admin", func(t *testing.T) { t.Run("set and delete by admin", func(t *testing.T) {
cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext")
cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT))
cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT)) cAdmin.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.TXT))
}) })
t.Run("set admin to null", func(t *testing.T) { t.Run("set admin to null", func(t *testing.T) {
cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext")
cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil)
cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT))
}) })
} }
func TestTransfer(t *testing.T) { func TestTransfer(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, true)
e := c.Executor e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
from := e.NewAccount(t) from := e.NewAccount(t)
cFrom := c.WithSigners(from) cFrom := c.WithSigners(from)
cFromCommittee := c.WithSigners(from, c.Committee)
to := e.NewAccount(t) to := e.NewAccount(t)
cTo := c.WithSigners(to) cTo := c.WithSigners(to)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash(), mail, refresh, retry, expire, ttl)
cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash()) cFrom.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4")
cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil)
c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, 1, "totalSupply") cFrom.Invoke(t, 2, "totalSupply") // com, neo.com
cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com") cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com")
// without onNEP11Transfer // without onNEP11Transfer
@ -358,30 +403,32 @@ func TestTransfer(t *testing.T) {
&compiler.Options{Name: "foo"}) &compiler.Options{Name: "foo"})
e.DeployContract(t, ctr, nil) e.DeployContract(t, ctr, nil)
cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil) cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil)
cFrom.Invoke(t, 1, "totalSupply") cFrom.Invoke(t, 2, "totalSupply") // com, neo.com
cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com")) cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com"))
} }
func TestTokensOf(t *testing.T) { func TestTokensOf(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, false)
e := c.Executor e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc1 := e.NewAccount(t) acc1 := e.NewAccount(t)
cAcc1 := c.WithSigners(acc1) cAcc1Committee := c.WithSigners(acc1, c.Committee)
acc2 := e.NewAccount(t) acc2 := e.NewAccount(t)
cAcc2 := c.WithSigners(acc2) cAcc2Committee := c.WithSigners(acc2, c.Committee)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") tld := []byte("com")
cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) c.Invoke(t, true, "register", tld, c.CommitteeHash, mail, refresh, retry, expire, ttl)
cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) cAcc1Committee.Invoke(t, true, "register", "neo.com", acc1.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc2Committee.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash(), mail, refresh, retry, expire, ttl)
testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) testTokensOf(t, c, tld, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE())
testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) testTokensOf(t, c, tld, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE())
testTokensOf(t, c, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) testTokensOf(t, c, tld, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still testTokensOf(t, c, tld, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still
} }
func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, args ...interface{}) { func testTokensOf(t *testing.T, c *neotest.ContractInvoker, tld []byte, result [][]byte, args ...interface{}) {
method := "tokensOf" method := "tokensOf"
if len(args) == 0 { if len(args) == 0 {
method = "tokens" method = "tokens"
@ -399,31 +446,195 @@ func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, arg
require.Equal(t, result[i], iter.Value().Value()) require.Equal(t, result[i], iter.Value().Value())
arr = append(arr, stackitem.Make(result[i])) arr = append(arr, stackitem.Make(result[i]))
} }
require.False(t, iter.Next()) if method == "tokens" {
require.True(t, iter.Next())
require.Equal(t, tld, iter.Value().Value())
} else {
require.False(t, iter.Next())
}
} }
func TestResolve(t *testing.T) { func TestResolve(t *testing.T) {
c := newNSClient(t) c := newNSClient(t, true)
e := c.Executor e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t) acc := e.NewAccount(t)
cAcc := c.WithSigners(acc) cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee)
c.Invoke(t, stackitem.Null{}, "addRoot", "com") cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com")
cAcc.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt from alias1")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.CNAME), "alias2.com")
c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) cAccCommittee.Invoke(t, true, "register", "alias2.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias2.com", int64(nns.TXT), "sometxt from alias2")
c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT))
c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com.", int64(nns.A))
c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.A))
// Check CNAME is properly resolved and is not included into the result.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("sometxt from alias1"), stackitem.Make("sometxt from alias2")}), "resolve", "neo.com", int64(nns.TXT))
// Check CNAME is included into the result and is not resolved.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("alias.com")}), "resolve", "neo.com", int64(nns.CNAME))
// Empty result.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "neo.com", int64(nns.AAAA))
}
func TestGetAllRecords(t *testing.T) {
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee)
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "bla0")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 0, "bla1") // overwrite
time := e.TopBlock(t).Timestamp
// Add some arbitrary data.
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt")
script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com")
require.NoError(t, err)
h := e.InvokeScript(t, script, []neotest.Signer{acc})
e.CheckHalt(t, h, stackitem.NewArray([]stackitem.Item{
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.A),
stackitem.NewByteArray([]byte("1.2.3.4")),
stackitem.NewBigInteger(big.NewInt(0)),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.CNAME),
stackitem.NewByteArray([]byte("alias.com")),
stackitem.NewBigInteger(big.NewInt(0)),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.SOA),
stackitem.NewBuffer([]byte("neo.com" + " " + mail + " " +
strconv.Itoa(int(time)) + " " + strconv.Itoa(int(refresh)) + " " +
strconv.Itoa(int(retry)) + " " + strconv.Itoa(int(expire)) + " " +
strconv.Itoa(int(ttl)))),
stackitem.NewBigInteger(big.NewInt(0)),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.TXT),
stackitem.NewByteArray([]byte("bla1")),
stackitem.NewBigInteger(big.NewInt(0)),
}),
}))
}
func TestGetRecords(t *testing.T) {
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee)
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com")
// Add some arbitrary data.
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
// Check empty result of `getRecords`.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.AAAA))
}
func TestNNSAddRecord(t *testing.T) {
c := newNSClient(t, true)
cAccCommittee := c.WithSigners(c.Committee)
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash, mail, refresh, retry, expire, ttl)
for i := 0; i <= maxRecordID+1; i++ {
if i == maxRecordID+1 {
c.InvokeFail(t, "maximum number of records reached", "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i))
} else {
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i))
}
}
}
func TestNNSRegisterArbitraryLevelDomain(t *testing.T) {
c := newNSClient(t, true)
newArgs := func(domain string, account neotest.Signer) []interface{} {
return []interface{}{
domain, account.ScriptHash(), "doesnt@matter.com",
int64(101), int64(102), int64(103), int64(104),
}
}
acc := c.NewAccount(t)
cBoth := c.WithSigners(c.Committee, acc)
args := newArgs("neo.com", acc)
cBoth.Invoke(t, true, "register", args...)
c1 := c.WithSigners(acc)
// parent domain is missing
args[0] = "testnet.fs.neo.com"
c1.InvokeFail(t, "one of the parent domains is not registered", "register", args...)
args[0] = "fs.neo.com"
c1.Invoke(t, true, "register", args...)
args[0] = "testnet.fs.neo.com"
c1.Invoke(t, true, "register", args...)
acc2 := c.NewAccount(t)
c2 := c.WithSigners(c.Committee, acc2)
args = newArgs("mainnet.fs.neo.com", acc2)
c2.InvokeFail(t, "not witnessed by admin", "register", args...)
c1.Invoke(t, stackitem.Null{}, "addRecord",
"something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4")
c1.Invoke(t, stackitem.Null{}, "addRecord",
"another.fs.neo.com", int64(nns.A), "4.3.2.1")
c2 = c.WithSigners(acc, acc2)
c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.fs.neo.com")
c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com",
"register", args...)
c1.Invoke(t, stackitem.Null{}, "deleteRecords",
"something.mainnet.fs.neo.com", int64(nns.A))
c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.fs.neo.com")
c2.Invoke(t, true, "register", args...)
c2 = c.WithSigners(acc2)
c2.Invoke(t, stackitem.Null{}, "addRecord",
"cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13")
result := stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray([]byte("166.15.14.13")),
})
c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A))
} }
const ( const (
defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceDomainPrice = 10_0000_0000
defaultNameServiceSysfee = 6000_0000 defaultNameServiceSysfee = 6000_0000
maxRecordID = 255
) )

View file

@ -9,6 +9,8 @@ const (
A RecordType = 1 A RecordType = 1
// CNAME represents canonical name record type. // CNAME represents canonical name record type.
CNAME RecordType = 5 CNAME RecordType = 5
// SOA represents start of authority record type.
SOA RecordType = 6
// TXT represents text record type. // TXT represents text record type.
TXT RecordType = 16 TXT RecordType = 16
) )

View file

@ -21,7 +21,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const neoAmount = 99999000 const (
neoAmount = 99999000
millisecondsInYear = 365 * 24 * 3600 * 1000
)
// Init pushes some predefined set of transactions into the given chain, it needs a path to // Init pushes some predefined set of transactions into the given chain, it needs a path to
// the root project directory. // the root project directory.
@ -158,17 +161,19 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) {
_, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, nsConfigPath, 4) // block #11 _, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, nsConfigPath, 4) // block #11
nsCommitteeInvoker := e.CommitteeInvoker(nsHash) nsCommitteeInvoker := e.CommitteeInvoker(nsHash)
nsPriv0Invoker := e.NewInvoker(nsHash, acc0) nsPriv0Invoker := e.NewInvoker(nsHash, acc0)
nsPriv0CommitteeInvoker := e.NewInvoker(nsHash, acc0, e.Committee)
// Block #12: transfer funds to committee for further NS record registration. // Block #12: transfer funds to committee for further NS record registration.
gasValidatorInvoker.Invoke(t, true, "transfer", gasValidatorInvoker.Invoke(t, true, "transfer",
e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12 e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12
// Block #13: add `.com` root to NNS. // Block #13: add `.com` root to NNS.
nsCommitteeInvoker.Invoke(t, stackitem.Null{}, "addRoot", "com") // block #13 mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
nsCommitteeInvoker.Invoke(t, true, "register", "com", nsCommitteeInvoker.CommitteeHash, mail, refresh, retry, expire, ttl) // block #13
// Block #14: register `neo.com` via NNS. // Block #14: register `neo.com` via NNS.
registerTxH := nsPriv0Invoker.Invoke(t, true, "register", registerTxH := nsPriv0CommitteeInvoker.Invoke(t, true, "register",
"neo.com", priv0ScriptHash) // block #14 "neo.com", priv0ScriptHash, mail, refresh, retry, expire, ttl) // block #14
res := e.GetTxExecResult(t, registerTxH) res := e.GetTxExecResult(t, registerTxH)
require.Equal(t, 1, len(res.Events)) // transfer require.Equal(t, 1, len(res.Events)) // transfer
tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes() tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes()
@ -176,7 +181,7 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) {
t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID)) t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID))
// Block #15: set A record type with priv0 owner via NNS. // Block #15: set A record type with priv0 owner via NNS.
nsPriv0Invoker.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 nsPriv0Invoker.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15
// Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call
txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1 txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1

View file

@ -1370,7 +1370,7 @@ func TestClient_NEP11_ND(t *testing.T) {
t.Run("TotalSupply", func(t *testing.T) { t.Run("TotalSupply", func(t *testing.T) {
s, err := n11.TotalSupply() s, err := n11.TotalSupply()
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, big.NewInt(1), s) // the only `neo.com` of acc0 require.EqualValues(t, big.NewInt(2), s) // `neo.com` of acc0 and TLD `com` of committee
}) })
t.Run("Symbol", func(t *testing.T) { t.Run("Symbol", func(t *testing.T) {
sym, err := n11.Symbol() sym, err := n11.Symbol()
@ -1403,14 +1403,14 @@ func TestClient_NEP11_ND(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
items, err := iter.Next(config.DefaultMaxIteratorResultItems) items, err := iter.Next(config.DefaultMaxIteratorResultItems)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(items)) require.Equal(t, 2, len(items))
require.Equal(t, [][]byte{[]byte("neo.com")}, items) require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items)
require.NoError(t, iter.Terminate()) require.NoError(t, iter.Terminate())
}) })
t.Run("TokensExpanded", func(t *testing.T) { t.Run("TokensExpanded", func(t *testing.T) {
items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems) items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("neo.com")}, items) require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items)
}) })
t.Run("Properties", func(t *testing.T) { t.Run("Properties", func(t *testing.T) {
p, err := n11.Properties([]byte("neo.com")) p, err := n11.Properties([]byte("neo.com"))
@ -1421,6 +1421,7 @@ func TestClient_NEP11_ND(t *testing.T) {
expected := stackitem.NewMap() expected := stackitem.NewMap()
expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com"))) expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com")))
expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula
expected.Add(stackitem.Make([]byte("admin")), stackitem.Null{})
require.EqualValues(t, expected, p) require.EqualValues(t, expected, p)
}) })
t.Run("Transfer", func(t *testing.T) { t.Run("Transfer", func(t *testing.T) {

View file

@ -74,12 +74,12 @@ const (
verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c" verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c"
verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A="
verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781" verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781"
nnsContractHash = "bdbfe1a280a0e23ca5b569c8f5845169bd93cb06" nnsContractHash = "cb93bcab0d6d435b61fa96a3bbce3b6f043968b5"
nnsToken1ID = "6e656f2e636f6d" nnsToken1ID = "6e656f2e636f6d"
nfsoContractHash = "0e15ca0df00669a2cd5dcb03bfd3e2b3849c2969" nfsoContractHash = "0e15ca0df00669a2cd5dcb03bfd3e2b3849c2969"
nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486"
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "f1380226a217b5e35ea968d42c50e20b9af7ab83b91416c8fb85536c61004332" block20StateRootLE = "7f80c7e265a44faa7374953d4d5059d21b34e65e06a7695d57ca8c59cc9a36fa"
storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7" storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7"
) )
@ -287,6 +287,7 @@ var rpcTestCases = map[string][]rpcTestCase{
return &map[string]interface{}{ return &map[string]interface{}{
"name": "neo.com", "name": "neo.com",
"expiration": "lhbLRl0B", "expiration": "lhbLRl0B",
"admin": nil, // no admin was set
} }
}, },
}, },
@ -935,7 +936,7 @@ var rpcTestCases = map[string][]rpcTestCase{
chg := []dboper.Operation{{ chg := []dboper.Operation{{
State: "Changed", State: "Changed",
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb},
Value: []byte{0xf6, 0x8b, 0x4e, 0x9d, 0x51, 0x79, 0x12}, Value: []byte{0x6e, 0xaf, 0xba, 0x5e, 0x51, 0x79, 0x12},
}, { }, {
State: "Added", State: "Added",
Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb},
@ -947,7 +948,7 @@ var rpcTestCases = map[string][]rpcTestCase{
}, { }, {
State: "Changed", State: "Changed",
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
Value: []byte{0x41, 0x01, 0x21, 0x05, 0xe4, 0x74, 0xef, 0xdb, 0x08}, Value: []byte{0x41, 0x01, 0x21, 0x05, 0xda, 0xb5, 0x8c, 0xda, 0x08},
}} }}
// Can be returned in any order. // Can be returned in any order.
assert.ElementsMatch(t, chg, res.Diagnostics.Changes) assert.ElementsMatch(t, chg, res.Diagnostics.Changes)
@ -963,7 +964,7 @@ var rpcTestCases = map[string][]rpcTestCase{
cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
return &result.Invoke{ return &result.Invoke{
State: "HALT", State: "HALT",
GasConsumed: 15928320, GasConsumed: 22192980,
Script: script, Script: script,
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Notifications: []state.NotificationEvent{}, Notifications: []state.NotificationEvent{},
@ -975,6 +976,15 @@ var rpcTestCases = map[string][]rpcTestCase{
{ {
Current: nnsHash, Current: nnsHash,
Calls: []*invocations.Tree{ Calls: []*invocations.Tree{
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: stdHash,
},
{ {
Current: stdHash, Current: stdHash,
}, },
@ -1078,7 +1088,7 @@ var rpcTestCases = map[string][]rpcTestCase{
cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
return &result.Invoke{ return &result.Invoke{
State: "HALT", State: "HALT",
GasConsumed: 15928320, GasConsumed: 22192980,
Script: script, Script: script,
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Notifications: []state.NotificationEvent{}, Notifications: []state.NotificationEvent{},
@ -1090,6 +1100,15 @@ var rpcTestCases = map[string][]rpcTestCase{
{ {
Current: nnsHash, Current: nnsHash,
Calls: []*invocations.Tree{ Calls: []*invocations.Tree{
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: stdHash,
},
{ {
Current: stdHash, Current: stdHash,
}, },
@ -2717,7 +2736,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
}, },
{ {
Asset: e.chain.UtilityTokenHash(), Asset: e.chain.UtilityTokenHash(),
Amount: "37099660700", Amount: "37076412050",
LastUpdated: 22, LastUpdated: 22,
Decimals: 8, Decimals: 8,
Name: "GasToken", Name: "GasToken",

Binary file not shown.