diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 8f67beecbc..aeff8dcdfc 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -29,7 +29,8 @@ const ( // prefixAccountToken contains map from (owner + token key) to token ID, // where token key = hash160(token ID) and token ID = domain name. prefixAccountToken byte = 0x02 - // prefixRegisterPrice contains price for new domain name registration. + // prefixRegisterPrice contains list of prices for new domain name registration + // depending on the domain name length. prefixRegisterPrice byte = 0x10 // prefixRoot contains set of roots (map from root to 0). prefixRoot byte = 0x20 @@ -47,22 +48,29 @@ const ( maxRegisterPrice = 1_0000_0000_0000 // maxRootLength is the maximum domain root length. maxRootLength = 16 - // maxDomainNameFragmentLength is the maximum length of the domain name fragment. - maxDomainNameFragmentLength = 62 + // maxDomainNameFragmentLength is the maximum length of the domain name fragment + maxDomainNameFragmentLength = 63 // minDomainNameLength is minimum domain length. minDomainNameLength = 3 // maxDomainNameLength is maximum domain length. maxDomainNameLength = 255 // maxTXTRecordLength is the maximum length of the TXT domain record. 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. const ( // defaultRegisterPrice is the default price for new domain registration. defaultRegisterPrice = 10_0000_0000 + // millisecondsInSecond is the amount of milliseconds per second. + millisecondsInSecond = 1000 // millisecondsInYear is amount of milliseconds per year. - millisecondsInYear = 365 * 24 * 3600 * 1000 + millisecondsInYear = 365 * 24 * 3600 * millisecondsInSecond + // millisecondsInTenYears is the amount of milliseconds per ten years. + millisecondsInTenYears = 10 * millisecondsInYear ) // RecordState is a type that registered entities are saved to. @@ -70,6 +78,7 @@ type RecordState struct { Name string Type RecordType Data string + ID byte } // Update updates NameService contract. @@ -85,7 +94,13 @@ func _deploy(data interface{}, isUpdate bool) { } ctx := storage.GetContext() storage.Put(ctx, []byte{prefixTotalSupply}, 0) - storage.Put(ctx, []byte{prefixRegisterPrice}, defaultRegisterPrice) + storage.Put(ctx, []byte{prefixRegisterPrice}, std.Serialize([]int{ + defaultRegisterPrice, // Prices for all other lengths of domain names. + -1, // Domain names with a length of 1 are not open for registration by default. + -1, // Domain names with a length of 2 are not open for registration by default. + -1, // Domain names with a length of 3 are not open for registration by default. + -1, // Domain names with a length of 4 are not open for registration by default. + })) } // Symbol returns NeoNameService symbol. @@ -118,6 +133,7 @@ func Properties(tokenID []byte) map[string]interface{} { return map[string]interface{}{ "name": ns.Name, "expiration": ns.Expiration, + "admin": ns.Admin, } } @@ -179,22 +195,6 @@ func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool { 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. func Roots() iterator.Iterator { ctx := storage.GetReadOnlyContext() @@ -202,48 +202,126 @@ func Roots() iterator.Iterator { } // SetPrice sets the domain registration price. -func SetPrice(price int) { +func SetPrice(priceList []int) { checkCommittee() - if price < 0 || price > maxRegisterPrice { - panic("The price is out of range.") + if len(priceList) == 0 { + panic("price list is empty") + } + for i := 0; i < len(priceList); i++ { + if i == 0 && priceList[i] == -1 { + panic("default price is out of range") + } + if priceList[i] < -1 || priceList[i] > maxRegisterPrice { + panic("price is out of range") + } } ctx := storage.GetContext() - storage.Put(ctx, []byte{prefixRegisterPrice}, price) + storage.Put(ctx, []byte{prefixRegisterPrice}, std.Serialize(priceList)) } -// GetPrice returns the domain registration price. -func GetPrice() int { +// GetPrice returns the domain registration price depending on the domain name length. +func GetPrice(length int) int { ctx := storage.GetReadOnlyContext() - return storage.Get(ctx, []byte{prefixRegisterPrice}).(int) + priceList := std.Deserialize(storage.Get(ctx, []byte{prefixRegisterPrice}).([]byte)).([]int) + if length >= len(priceList) { + length = 0 + } + return priceList[length] } // IsAvailable checks whether provided domain name is available. func IsAvailable(name string) bool { - fragments := splitAndCheck(name, false) + fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } ctx := storage.GetReadOnlyContext() - if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil { - panic("root not found") - } - nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...)) - if nsBytes == nil { + l := len(fragments) + if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[l-1])...)) == nil { + if l != 1 { + panic("TLD not found") + } return true } - ns := std.Deserialize(nsBytes.([]byte)).(NameState) - return runtime.GetTime() >= ns.Expiration + if GetPrice(len(fragments[0])) < 0 { + return false + } + if !parentExpired(ctx, 0, fragments) { + 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. -func Register(name string, owner interop.Hash160) bool { - fragments := splitAndCheck(name, false) +func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool { + fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } + l := len(fragments) + tldKey := append([]byte{prefixRoot}, []byte(fragments[l-1])...) ctx := storage.GetContext() - if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil { - panic("root not found") + tldBytes := storage.Get(ctx, tldKey) + 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) { @@ -252,7 +330,13 @@ func Register(name string, owner interop.Hash160) bool { if !runtime.CheckWitness(owner) { panic("not witnessed by owner") } - runtime.BurnGas(GetPrice()) + price := GetPrice(len(fragments[0])) + if price < 0 { + checkCommittee() + } else { + runtime.BurnGas(price) + } + var ( tokenKey = getTokenKey([]byte(name)) oldOwner interop.Hash160 @@ -275,24 +359,95 @@ func Register(name string, owner interop.Hash160) bool { ns := NameState{ Owner: owner, Name: name, - Expiration: runtime.GetTime() + millisecondsInYear, + Expiration: runtime.GetTime() + expire*millisecondsInSecond, } putNameStateWithKey(ctx, tokenKey, ns) + putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) updateBalance(ctx, []byte(name), owner, +1) postTransfer(oldOwner, owner, []byte(name), nil) return true } -// Renew increases domain expiration date. -func Renew(name string) int { +// 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) +} + +// RenewDefault increases domain expiration date up to 1 year. +func RenewDefault(name string) int { + return Renew(name, 1) +} + +// Renew increases domain expiration date up to the specified amount of years. +func Renew(name string, years int64) int { + if years < 1 || years > 10 { + panic("invalid renewal period value") + } if len(name) > maxDomainNameLength { panic("invalid domain name format") } - runtime.BurnGas(GetPrice()) + fragments := splitAndCheck(name, true) + if fragments == nil { + panic("invalid domain name format") + } + price := GetPrice(len(fragments[0])) + if price < 0 { + checkCommittee() + } else { + runtime.BurnGas(price * int(years)) + } + ctx := storage.GetContext() ns := getNameState(ctx, []byte(name)) - ns.Expiration += millisecondsInYear + oldExpiration := ns.Expiration + ns.Expiration += int(millisecondsInYear * years) + if ns.Expiration > int(runtime.GetTime())+millisecondsInTenYears { + panic("10 years of expiration period at max is allowed") + } putNameState(ctx, ns) + runtime.Notify("Renew", name, oldExpiration, ns.Expiration) return ns.Expiration } @@ -309,13 +464,52 @@ func SetAdmin(name string, admin interop.Hash160) { if !runtime.CheckWitness(ns.Owner) { panic("not witnessed by owner") } + oldAdm := ns.Admin ns.Admin = admin putNameState(ctx, ns) + runtime.Notify("SetAdmin", name, oldAdm, admin) +} + +// SetRecord updates record of the specified type and ID. +func SetRecord(name string, typ RecordType, id byte, data string) { + 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) } -// SetRecord adds new record of the specified type to the provided domain. -func SetRecord(name string, typ RecordType, data string) { - tokenID := []byte(tokenIDFromName(name)) +// 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 switch typ { case A: @@ -332,44 +526,50 @@ func SetRecord(name string, typ RecordType, data string) { if !ok { panic("invalid record data") } - ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - putRecord(ctx, tokenID, name, typ, data) + return tokenID } -// GetRecord returns domain record of the specified type if it exists or an empty -// string if not. -func GetRecord(name string, typ RecordType) string { - tokenID := []byte(tokenIDFromName(name)) +// GetRecords returns domain records of the specified type if they exist or an empty +// array if not. +func GetRecords(name string, typ RecordType) []string { ctx := storage.GetReadOnlyContext() + tokenID := []byte(tokenIDFromName(ctx, name)) _ = 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. -func DeleteRecord(name string, typ RecordType) { - tokenID := []byte(tokenIDFromName(name)) +// DeleteRecords removes all domain records with the specified type. +func DeleteRecords(name string, typ RecordType) { + if typ == SOA { + panic("forbidden to delete SOA record") + } ctx := storage.GetContext() + tokenID := []byte(tokenIDFromName(ctx, name)) ns := getNameState(ctx, tokenID) ns.checkAdmin() - recordKey := getRecordKey(tokenID, name, typ) - storage.Delete(ctx, recordKey) + recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) + 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). -func Resolve(name string, typ RecordType) string { +// Resolve resolves given name (not more than three redirects are allowed) to a set +// of domain records. +func Resolve(name string, typ RecordType) []string { 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. func GetAllRecords(name string) iterator.Iterator { - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() - _ = getNameState(ctx, tokenID) // ensure not expired - recordsKey := getRecordsKey(tokenID, name) - return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) + return getAllRecords(ctx, name) } // updateBalance updates account's balance and account's tokens. @@ -425,12 +625,17 @@ func getTokenKey(tokenID []byte) []byte { // getNameState returns domain name state by the specified tokenID. func getNameState(ctx storage.Context, tokenID []byte) NameState { 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. func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { - nameKey := append([]byte{prefixName}, tokenKey...) + nameKey := getNameStateKey(tokenKey) nsBytes := storage.Get(ctx, nameKey) if nsBytes == nil { panic("token not found") @@ -440,6 +645,11 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { 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. func putNameState(ctx storage.Context, ns NameState) { tokenKey := getTokenKey([]byte(ns.Name)) @@ -453,41 +663,53 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) { storage.Put(ctx, nameKey, nsBytes) } -// getRecord returns domain record. -func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string { - recordKey := getRecordKey(tokenId, name, typ) - recBytes := storage.Get(ctx, recordKey) - if recBytes == nil { - return recBytes.(string) // A hack to actually return NULL. +// getRecordsByType returns domain records of the specified type or an empty array if no records found. +func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string { + recordsPrefix := getRecordsByTypePrefix(tokenId, name, typ) + records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues) + res := []string{} // return empty slice if no records was found. + 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 record.Data + return res } -// putRecord stores domain record. -func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) { - recordKey := getRecordKey(tokenId, name, typ) +// putRecord puts the specified record to the contract storage without any additional checks. +func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) { + recordKey := getRecordKey(tokenId, name, typ, id) rs := RecordState{ Name: name, Type: typ, - Data: record, + Data: data, + ID: id, } recBytes := std.Serialize(rs) storage.Put(ctx, recordKey, recBytes) } -// getRecordsKey returns prefix used to store domain records of different types. -func getRecordsKey(tokenId []byte, name string) []byte { - recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) - return append(recordKey, getTokenKey([]byte(name))...) +// getRecordKey returns key used to store domain record with the specified type and ID. +// This key always have a single corresponding value. +func getRecordKey(tokenId []byte, name string, typ RecordType, id byte) []byte { + prefix := getRecordsByTypePrefix(tokenId, name, typ) + return append(prefix, id) } -// getRecordKey returns key used to store domain records. -func getRecordKey(tokenId []byte, name string, typ RecordType) []byte { - recordKey := getRecordsKey(tokenId, name) +// getRecordsByTypePrefix returns prefix used to store domain records with the +// specified type of different IDs. +func getRecordsByTypePrefix(tokenId []byte, name string, typ RecordType) []byte { + recordKey := getRecordsPrefix(tokenId, name) 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. func isValid(address interop.Hash160) bool { return address != nil && len(address) == 20 @@ -507,6 +729,8 @@ func checkCommittee() { } // 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 { maxLength := maxDomainNameFragmentLength if isRoot { @@ -525,12 +749,12 @@ func checkFragment(v string, isRoot bool) bool { return false } } - for i := 1; i < len(v); i++ { - if !isAlNum(v[i]) { + for i := 1; i < len(v)-1; i++ { + if v[i] != '-' && !isAlNum(v[i]) { return false } } - return true + return isAlNum(v[len(v)-1]) } // isAlNum checks whether provided char is a lowercase letter or a number. @@ -546,9 +770,6 @@ func splitAndCheck(name string, allowMultipleFragments bool) []string { } fragments := std.StringSplit(name, ".") l = len(fragments) - if l < 2 { - return nil - } if l > 2 && !allowMultipleFragments { return nil } @@ -671,48 +892,66 @@ func checkIPv6(data string) bool { } // 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) if fragments == nil { panic("invalid domain name format") } - l := len(fragments) - return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):] + sum := 0 + 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 // 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 { 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 := "" for iterator.Next(records) { - r := iterator.Value(records).(struct { - key string - rs RecordState - }) - value := r.rs.Data - rTyp := r.key[len(r.key)-1] - if rTyp == byte(typ) { - return value + r := iterator.Value(records).(RecordState) + if r.Type == typ { + res = append(res, r.Data) } - if rTyp == byte(CNAME) { - cname = value + if r.Type == CNAME { + cname = r.Data } } - if cname == "" { - return string([]byte(nil)) + if cname == "" || typ == CNAME { + 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 -// specified name. -func getRecords(ctx storage.Context, name string) iterator.Iterator { - tokenID := []byte(tokenIDFromName(name)) - _ = getNameState(ctx, tokenID) - recordsKey := getRecordsKey(tokenID, name) - return storage.Find(ctx, recordsKey, storage.DeserializeValues) +// getAllRecords returns iterator over the set of records corresponded with the +// specified name. Records returned are of different types and/or different IDs. +// No keys are returned. +func getAllRecords(ctx storage.Context, name string) iterator.Iterator { + tokenID := []byte(tokenIDFromName(ctx, name)) + _ = getNameState(ctx, tokenID) // ensure not expired. + recordsPrefix := getRecordsPrefix(tokenID, name) + return storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues) } diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 1f24f3bc93..4d09e6d542 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -2,7 +2,7 @@ name: "NameService" sourceurl: https://github.com/nspcc-dev/neo-go/ supportedstandards: ["NEP-11"] safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", - "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", + "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", "resolve", "getAllRecords"] events: - name: Transfer @@ -15,7 +15,25 @@ events: type: Integer - name: tokenId type: ByteArray + - name: SetAdmin + parameters: + - name: name + type: String + - name: oldAdmin + type: Hash160 + - name: newAdmin + type: Hash160 + - name: Renew + parameters: + - name: name + type: String + - name: oldExpiration + type: Integer + - name: newExpiration + type: Integer permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd methods: ["update"] - methods: ["onNEP11Payment"] +overloads: + renewDefault: renew diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index dcd0f6993d..b294885ba1 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -1,68 +1,95 @@ package nns_test import ( + "math/big" + "strconv" "strings" "testing" nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" "github.com/nspcc-dev/neo-go/pkg/compiler" "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/neotest" "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/vm/stackitem" "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) e := neotest.NewExecutor(t, bc, acc, acc) - c := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") - e.DeployContract(t, c, nil) + ctr := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") + 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) { const ( - minPrice = int64(0) - maxPrice = int64(10000_00000000) + minPrice = int64(-1) + maxPrice = int64(10000_00000000) + defaultPrice = 10_0000_0000 + committeePrice = -1 ) - c := newNSClient(t) + c := newNSClient(t, false) t.Run("set, not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) cAcc := c.WithSigners(acc) cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1) }) - t.Run("get, default value", func(t *testing.T) { - c.Invoke(t, defaultNameServiceDomainPrice, "getPrice") + c.Invoke(t, defaultPrice, "getPrice", 0) + c.Invoke(t, committeePrice, "getPrice", 1) + c.Invoke(t, committeePrice, "getPrice", 2) + c.Invoke(t, committeePrice, "getPrice", 3) + c.Invoke(t, committeePrice, "getPrice", 4) + c.Invoke(t, defaultPrice, "getPrice", 5) }) - t.Run("set, too small value", func(t *testing.T) { - c.InvokeFail(t, "The price is out of range.", "setPrice", minPrice-1) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{minPrice - 1}) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{defaultPrice, minPrice - 1}) }) - t.Run("set, too large value", func(t *testing.T) { - c.InvokeFail(t, "The price is out of range.", "setPrice", maxPrice+1) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{minPrice - 1}) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{defaultPrice, minPrice - 1}) + }) + t.Run("set, negative default price", func(t *testing.T) { + c.InvokeFail(t, "default price is out of range", "setPrice", []interface{}{committeePrice, minPrice + 1}) }) - t.Run("set, success", func(t *testing.T) { - txSet := c.PrepareInvoke(t, "setPrice", int64(defaultNameServiceDomainPrice+1)) - txGet := c.PrepareInvoke(t, "getPrice") - c.AddBlockCheckHalt(t, txSet, txGet) + txSet := c.PrepareInvoke(t, "setPrice", []interface{}{defaultPrice - 1, committeePrice, committeePrice, committeePrice, committeePrice, committeePrice}) + txGet1 := c.PrepareInvoke(t, "getPrice", 5) + txGet2 := c.PrepareInvoke(t, "getPrice", 6) + c.AddBlockCheckHalt(t, txSet, txGet1, txGet2) c.CheckHalt(t, txSet.Hash(), stackitem.Null{}) - c.CheckHalt(t, txGet.Hash(), stackitem.Make(defaultNameServiceDomainPrice+1)) + c.CheckHalt(t, txGet1.Hash(), stackitem.Make(committeePrice)) + c.CheckHalt(t, txGet2.Hash(), stackitem.Make(defaultPrice-1)) // Get in the next block. - c.Invoke(t, stackitem.Make(defaultNameServiceDomainPrice+1), "getPrice") + c.Invoke(t, stackitem.Make(committeePrice), "getPrice", 2) + c.Invoke(t, stackitem.Make(committeePrice), "getPrice", 5) + c.Invoke(t, stackitem.Make(defaultPrice-1), "getPrice", 6) }) } func TestNonfungible(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) c.Signers = []neotest.Signer{c.NewAccount(t)} c.Invoke(t, "NNS", "symbol") @@ -70,105 +97,110 @@ func TestNonfungible(t *testing.T) { c.Invoke(t, 0, "totalSupply") } -func TestAddRoot(t *testing.T) { - c := newNSClient(t) +func TestRegisterTLD(t *testing.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) { - 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) { acc := c.NewAccount(t) 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) { - 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) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor 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) 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") - cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") + cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "first.com", int64(nns.TXT), "sometext") 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.Index = b1.Index + 1 b2.PrevHash = b1.Hash() b2.Timestamp = b1.Timestamp + 10000 require.NoError(t, bc.AddBlock(e.SignBlock(b2))) - e.CheckHalt(t, tx.Hash()) + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) - tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com") - b3 := e.NewUnsignedBlock(t, tx) + b3 := e.NewUnsignedBlock(t) b3.Index = b2.Index + 1 b3.PrevHash = b2.Hash() - b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1) + b3.Timestamp = b1.Timestamp + (uint64(expire) * 1000) require.NoError(t, bc.AddBlock(e.SignBlock(b3))) - e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) - tx = cAcc.PrepareInvoke(t, "isAvailable", "second.com") - b4 := e.NewUnsignedBlock(t, tx) - b4.Index = b3.Index + 1 - b4.PrevHash = b3.Hash() - b4.Timestamp = b3.Timestamp + 1000 - 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)) - b5 := e.NewUnsignedBlock(t, tx) - b5.Index = b4.Index + 1 - 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. - cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT)) -} + cAcc.Invoke(t, true, "isAvailable", "first.com") // "first.com" has been expired + cAcc.Invoke(t, true, "isAvailable", "second.com") // TLD "com" has been expired + cAcc.InvokeFail(t, "name has expired", "getRecords", "first.com", int64(nns.TXT)) -const millisecondsInYear = 365 * 24 * 3600 * 1000 + // TODO: According to the new code, we can't re-register expired "com" TLD, because it's already registered; at the + // 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)) +} func TestRegisterAndRenew(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*2), int64(104) + + c.InvokeFail(t, "TLD not found", "isAvailable", "neo-go.com") + c.Invoke(t, true, "register", "org", c.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "TLD not found", "isAvailable", "neo-go.com") + c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl) + c.Invoke(t, true, "isAvailable", "neo-go.com") + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo-go.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo-go.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "\nneo-go.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "neo-go.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo-go.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo-go.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.InvokeFail(t, "root not found", "isAvailable", "neo.com") - c.Invoke(t, stackitem.Null{}, "addRoot", "org") - c.InvokeFail(t, "root not found", "isAvailable", "neo.com") - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - c.Invoke(t, true, "isAvailable", "neo.com") - c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) - c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash) - c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash) - c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash) - c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) - c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash) - - c.Invoke(t, true, "isAvailable", "neo.com") - c.Invoke(t, 0, "balanceOf", e.CommitteeHash) - c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) + c.Invoke(t, true, "isAvailable", "neo-go.com") + c.Invoke(t, 3, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com + c.Invoke(t, true, "register", "neo-go.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) topBlock := e.TopBlock(t) - expectedExpiration := topBlock.Timestamp + millisecondsInYear - c.Invoke(t, false, "register", "neo.com", e.CommitteeHash) - c.Invoke(t, false, "isAvailable", "neo.com") + expectedExpiration := topBlock.Timestamp + uint64(expire*1000) + c.Invoke(t, false, "register", "neo-go.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.Invoke(t, false, "isAvailable", "neo-go.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.Add(stackitem.Make("name"), stackitem.Make("neo.com")) + props.Add(stackitem.Make("name"), stackitem.Make("neo-go.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - c.Invoke(t, props, "properties", "neo.com") - c.Invoke(t, 1, "balanceOf", e.CommitteeHash) - c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) + props.Add(stackitem.Make("admin"), stackitem.Null{}) // no admin was set + c.Invoke(t, props, "properties", "neo-go.com") + 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-go.com")) t.Run("invalid token ID", func(t *testing.T) { c.InvokeFail(t, "token not found", "properties", "not.exists") @@ -178,49 +210,96 @@ func TestRegisterAndRenew(t *testing.T) { }) // Renew + oldExpiration := expectedExpiration expectedExpiration += millisecondsInYear - c.Invoke(t, expectedExpiration, "renew", "neo.com") + h := c.Invoke(t, expectedExpiration, "renew", "neo-go.com") + c.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "Renew", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo-go.com")), + stackitem.Make(oldExpiration), + stackitem.Make(expectedExpiration), + }), + }) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - c.Invoke(t, props, "properties", "neo.com") + c.Invoke(t, props, "properties", "neo-go.com") + + // Invalid renewal period. + c.InvokeFail(t, "invalid renewal period value", "renew", "neo-go.com", 11) + // Too large expiration period. + c.InvokeFail(t, "10 years of expiration period at max is allowed", "renew", "neo-go.com", 10) + + // Non-default renewal period. + oldExpiration = expectedExpiration + mult := 2 + expectedExpiration += uint64(mult * millisecondsInYear) + h = c.Invoke(t, expectedExpiration, "renew", "neo-go.com", mult) + c.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "Renew", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo-go.com")), + stackitem.Make(oldExpiration), + stackitem.Make(expectedExpiration), + }), + }) } -func TestSetGetRecord(t *testing.T) { - c := newNSClient(t) +func TestSetAddGetRecord(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) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") 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) { - c.InvokeFail(t, "unsupported record type", "setRecord", "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, "unsupported record type", "addRecord", "neo.com", int64(0xFF), "1.2.3.4") + c.InvokeFail(t, "invalid record", "addRecord", "neo.com", int64(nns.A), "not.an.ip.address") }) 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.Null{}, "setRecord", "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.Null{}, "setRecord", "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.Null{}, "setRecord", "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{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) + c.InvokeFail(t, "record already exists", "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") + 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. 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.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, stackitem.Null{}, "getRecord", "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("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.CNAME)) + 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) { // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior @@ -280,9 +359,10 @@ func TestSetGetRecord(t *testing.T) { args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name} t.Run(testCase.Name, func(t *testing.T) { if testCase.ShouldFail { - c.InvokeFail(t, "", "setRecord", args...) + c.InvokeFail(t, "", "addRecord", args...) } 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,56 +370,83 @@ func TestSetGetRecord(t *testing.T) { } func TestSetAdmin(t *testing.T) { - c := newNSClient(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) owner := e.NewAccount(t) cOwner := c.WithSigners(owner) + cOwnerCommittee := c.WithSigners(owner, c.Committee) admin := e.NewAccount(t) cAdmin := c.WithSigners(admin) guest := e.NewAccount(t) cGuest := c.WithSigners(guest) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - - cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash()) + 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) + expectedExpiration := e.TopBlock(t).Timestamp + uint64(expire)*1000 cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) // Must be witnessed by both owner and admin. cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.ScriptHash()) cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash()) cc := c.WithSigners(owner, admin) - cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) + h := cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) + cc.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: cc.Hash, + Name: "SetAdmin", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.Null{}, + stackitem.NewByteArray(admin.ScriptHash().BytesBE()), + }), + }) + + 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) { - cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") - cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) - cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") + cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecords", "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) { - cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") - cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) - cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") + h = cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) + cc.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: cc.Hash, + Name: "SetAdmin", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.NewByteArray(admin.ScriptHash().BytesBE()), + stackitem.Null{}, + }), + }) + cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT)) }) } func TestTransfer(t *testing.T) { - c := newNSClient(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) from := e.NewAccount(t) cFrom := c.WithSigners(from) + cFromCommittee := c.WithSigners(from, c.Committee) to := e.NewAccount(t) cTo := c.WithSigners(to) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash()) - cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash(), mail, refresh, retry, expire, ttl) + cFrom.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) c.Invoke(t, false, "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") // without onNEP11Transfer @@ -358,30 +465,32 @@ func TestTransfer(t *testing.T) { &compiler.Options{Name: "foo"}) e.DeployContract(t, ctr, 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")) } func TestTokensOf(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) 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) - cAcc1 := c.WithSigners(acc1) + cAcc1Committee := c.WithSigners(acc1, c.Committee) acc2 := e.NewAccount(t) - cAcc2 := c.WithSigners(acc2) + cAcc2Committee := c.WithSigners(acc2, c.Committee) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) - cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) + tld := []byte("com") + c.Invoke(t, true, "register", tld, c.CommitteeHash, mail, refresh, retry, expire, ttl) + 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, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) - testTokensOf(t, c, [][]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{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) + testTokensOf(t, c, tld, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) + testTokensOf(t, c, tld, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) + 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" if len(args) == 0 { method = "tokens" @@ -399,31 +508,196 @@ func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, arg require.Equal(t, result[i], iter.Value().Value()) 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) { - c := newNSClient(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") + + 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 from alias1") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.CNAME), "alias2.com") + + cAccCommittee.Invoke(t, true, "register", "alias2.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias2.com", int64(nns.TXT), "sometxt from alias2") + + 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)) +} - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") +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) - cAcc.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") + cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash, mail, refresh, retry, expire, ttl) - c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) - c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) - c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT)) - c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) + 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) + // Use long (>4 chars) domain name to avoid committee signature check. + // parent domain is missing + args[0] = "testnet.filestorage.neo.com" + c1.InvokeFail(t, "one of the parent domains is not registered", "register", args...) + + args[0] = "filestorage.neo.com" + c1.Invoke(t, true, "register", args...) + + args[0] = "testnet.filestorage.neo.com" + c1.Invoke(t, true, "register", args...) + + acc2 := c.NewAccount(t) + c2 := c.WithSigners(c.Committee, acc2) + args = newArgs("mainnet.filestorage.neo.com", acc2) + c2.InvokeFail(t, "not witnessed by admin", "register", args...) + + c1.Invoke(t, stackitem.Null{}, "addRecord", + "something.mainnet.filestorage.neo.com", int64(nns.A), "1.2.3.4") + c1.Invoke(t, stackitem.Null{}, "addRecord", + "another.filestorage.neo.com", int64(nns.A), "4.3.2.1") + + c2 = c.WithSigners(acc, acc2) + c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.filestorage.neo.com") + c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.filestorage.neo.com", + "register", args...) + + c1.Invoke(t, stackitem.Null{}, "deleteRecords", + "something.mainnet.filestorage.neo.com", int64(nns.A)) + c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.filestorage.neo.com") + c2.Invoke(t, true, "register", args...) + + c2 = c.WithSigners(acc2) + c2.Invoke(t, stackitem.Null{}, "addRecord", + "cdn.mainnet.filestorage.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.filestorage.neo.com", int64(nns.A)) } const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 + maxRecordID = 255 ) diff --git a/examples/nft-nd-nns/recordtype.go b/examples/nft-nd-nns/recordtype.go index 98035f34ac..52229c7a39 100644 --- a/examples/nft-nd-nns/recordtype.go +++ b/examples/nft-nd-nns/recordtype.go @@ -9,6 +9,8 @@ const ( A RecordType = 1 // CNAME represents canonical name record type. CNAME RecordType = 5 + // SOA represents start of authority record type. + SOA RecordType = 6 // TXT represents text record type. TXT RecordType = 16 ) diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index f0071cfd39..c22f782345 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -21,7 +21,10 @@ import ( "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 // 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 nsCommitteeInvoker := e.CommitteeInvoker(nsHash) nsPriv0Invoker := e.NewInvoker(nsHash, acc0) + nsPriv0CommitteeInvoker := e.NewInvoker(nsHash, acc0, e.Committee) // Block #12: transfer funds to committee for further NS record registration. gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12 // 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. - registerTxH := nsPriv0Invoker.Invoke(t, true, "register", - "neo.com", priv0ScriptHash) // block #14 + registerTxH := nsPriv0CommitteeInvoker.Invoke(t, true, "register", + "neo.com", priv0ScriptHash, mail, refresh, retry, expire, ttl) // block #14 res := e.GetTxExecResult(t, registerTxH) require.Equal(t, 1, len(res.Events)) // transfer 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)) // 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 txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1 diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index dc7fb11191..be3a23994e 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1370,7 +1370,7 @@ func TestClient_NEP11_ND(t *testing.T) { t.Run("TotalSupply", func(t *testing.T) { s, err := n11.TotalSupply() 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) { sym, err := n11.Symbol() @@ -1403,14 +1403,14 @@ func TestClient_NEP11_ND(t *testing.T) { require.NoError(t, err) items, err := iter.Next(config.DefaultMaxIteratorResultItems) require.NoError(t, err) - require.Equal(t, 1, len(items)) - require.Equal(t, [][]byte{[]byte("neo.com")}, items) + require.Equal(t, 2, len(items)) + require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items) require.NoError(t, iter.Terminate()) }) t.Run("TokensExpanded", func(t *testing.T) { items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems) 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) { p, err := n11.Properties([]byte("neo.com")) @@ -1421,6 +1421,7 @@ func TestClient_NEP11_ND(t *testing.T) { expected := stackitem.NewMap() 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("admin")), stackitem.Null{}) require.EqualValues(t, expected, p) }) t.Run("Transfer", func(t *testing.T) { diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index d5a9fce4a8..903f0db922 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -74,12 +74,12 @@ const ( verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c" verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781" - nnsContractHash = "bdbfe1a280a0e23ca5b569c8f5845169bd93cb06" + nnsContractHash = "cb93bcab0d6d435b61fa96a3bbce3b6f043968b5" nnsToken1ID = "6e656f2e636f6d" nfsoContractHash = "0e15ca0df00669a2cd5dcb03bfd3e2b3849c2969" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" - block20StateRootLE = "f1380226a217b5e35ea968d42c50e20b9af7ab83b91416c8fb85536c61004332" + block20StateRootLE = "7f80c7e265a44faa7374953d4d5059d21b34e65e06a7695d57ca8c59cc9a36fa" storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7" ) @@ -287,6 +287,7 @@ var rpcTestCases = map[string][]rpcTestCase{ return &map[string]interface{}{ "name": "neo.com", "expiration": "lhbLRl0B", + "admin": nil, // no admin was set } }, }, @@ -935,7 +936,7 @@ var rpcTestCases = map[string][]rpcTestCase{ chg := []dboper.Operation{{ State: "Changed", 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", 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", 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. assert.ElementsMatch(t, chg, res.Diagnostics.Changes) @@ -963,7 +964,7 @@ var rpcTestCases = map[string][]rpcTestCase{ cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 15928320, + GasConsumed: 22192980, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -975,6 +976,15 @@ var rpcTestCases = map[string][]rpcTestCase{ { Current: nnsHash, Calls: []*invocations.Tree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, { Current: stdHash, }, @@ -1078,7 +1088,7 @@ var rpcTestCases = map[string][]rpcTestCase{ cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 15928320, + GasConsumed: 22192980, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -1090,6 +1100,15 @@ var rpcTestCases = map[string][]rpcTestCase{ { Current: nnsHash, Calls: []*invocations.Tree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, { Current: stdHash, }, @@ -2717,7 +2736,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "37099660700", + Amount: "37076412050", LastUpdated: 22, Decimals: 8, Name: "GasToken", diff --git a/pkg/services/rpcsrv/testdata/testblocks.acc b/pkg/services/rpcsrv/testdata/testblocks.acc index 965230d6a4..2fcfdc437e 100644 Binary files a/pkg/services/rpcsrv/testdata/testblocks.acc and b/pkg/services/rpcsrv/testdata/testblocks.acc differ