diff --git a/src/NameService/NameService.cs b/src/NameService/NameService.cs index a16f829..8a6cb6b 100644 --- a/src/NameService/NameService.cs +++ b/src/NameService/NameService.cs @@ -52,8 +52,7 @@ public sealed class NameService : Framework.SmartContract public static UInt160 OwnerOf(ByteString tokenId) { StorageMap nameMap = new(Storage.CurrentContext, Prefix_Name); - NameState token = (NameState)StdLib.Deserialize(nameMap[GetKey(tokenId)]); - token.EnsureNotExpired(); + NameState token = getNameState(nameMap, tokenId); return token.Owner; } @@ -61,8 +60,7 @@ public static UInt160 OwnerOf(ByteString tokenId) public static Map Properties(ByteString tokenId) { StorageMap nameMap = new(Storage.CurrentContext, Prefix_Name); - NameState token = (NameState)StdLib.Deserialize(nameMap[GetKey(tokenId)]); - token.EnsureNotExpired(); + NameState token = getNameState(nameMap, tokenId); Map map = new(); map["name"] = token.Name; map["expiration"] = token.Expiration; @@ -142,17 +140,6 @@ public static void Update(ByteString nef, string manifest) ContractManagement.Update(nef, manifest); } - public static void AddRoot(string root) - { - CheckCommittee(); - if (!CheckFragment(root, true)) - throw new FormatException("The format of the root is incorrect."); - StorageMap rootMap = new(Storage.CurrentContext, Prefix_Root); - if (rootMap[root] is not null) - throw new InvalidOperationException("The root already exists."); - rootMap.Put(root, 0); - } - [Safe] public static Iterator Roots() { @@ -190,13 +177,13 @@ public static bool IsAvailable(string name) StorageMap nameMap = new(context, Prefix_Name); string[] fragments = SplitAndCheck(name, false); if (fragments is null) throw new FormatException("The format of the name is incorrect."); - if (rootMap[fragments[^1]] is null) throw new Exception("The root does not exist."); + if (rootMap[fragments[^1]] is null) { + if (fragments.Length != 1) throw new InvalidOperationException("The TLD is not found"); + return true; + } long price = GetPrice((byte)fragments[0].Length); if (price < 0) return false; - ByteString buffer = nameMap[GetKey(name)]; - if (buffer is null) return true; - NameState token = (NameState)StdLib.Deserialize(buffer); - return Runtime.Time >= token.Expiration; + return parentExpired(nameMap, 0, fragments); } public static bool Register(string name, UInt160 owner) @@ -208,7 +195,18 @@ public static bool Register(string name, UInt160 owner) StorageMap nameMap = new(context, Prefix_Name); string[] fragments = SplitAndCheck(name, false); if (fragments is null) throw new FormatException("The format of the name is incorrect."); - if (rootMap[fragments[^1]] is null) throw new Exception("The root does not exist."); + ByteString tld = rootMap[fragments[^1]]; + if (fragments.Length == 1) { + CheckCommittee(); + if (tld is not null) throw new InvalidOperationException("TLD already exists."); + rootMap.Put(fragments[^1], 0); + } else { + if (tld is null) throw new InvalidOperationException("TLD does not exist."); + if (parentExpired(nameMap, 1, fragments)) throw new InvalidOperationException("One of the parent domains has expired."); + ByteString parentKey = GetKey(fragments[1]); + NameState parent = (NameState)StdLib.Deserialize(nameMap[parentKey]); + parent.CheckAdmin(); + } if (!Runtime.CheckWitness(owner)) throw new InvalidOperationException("No authorization."); long price = GetPrice((byte)fragments[0].Length); if (price < 0) @@ -278,12 +276,11 @@ public static ulong Renew(string name, byte years) else Runtime.BurnGas(price * years); StorageMap nameMap = new(Storage.CurrentContext, Prefix_Name); - ByteString tokenKey = GetKey(name); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); + NameState token = getNameState(nameMap, name); token.Expiration += OneYear * years; if (token.Expiration > Runtime.Time + TenYears) throw new ArgumentException("You can't renew a domain name for more than 10 years in total."); + ByteString tokenKey = GetKey(name); nameMap[tokenKey] = StdLib.Serialize(token); return token.Expiration; } @@ -293,12 +290,11 @@ public static void SetAdmin(string name, UInt160 admin) if (name.Length > NameMaxLength) throw new FormatException("The format of the name is incorrect."); if (admin is not null && !Runtime.CheckWitness(admin)) throw new InvalidOperationException("No authorization."); StorageMap nameMap = new(Storage.CurrentContext, Prefix_Name); - ByteString tokenKey = GetKey(name); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); + NameState token = getNameState(nameMap, name); if (!Runtime.CheckWitness(token.Owner)) throw new InvalidOperationException("No authorization."); UInt160 old = token.Admin; token.Admin = admin; + ByteString tokenKey = GetKey(name); nameMap[tokenKey] = StdLib.Serialize(token); OnSetAdmin(name, old, admin); } @@ -308,8 +304,7 @@ public static void SetRecord(string name, RecordType type, string data) StorageContext context = Storage.CurrentContext; StorageMap nameMap = new(context, Prefix_Name); StorageMap recordMap = new(context, Prefix_Record); - string[] fragments = SplitAndCheck(name, true); - if (fragments is null) throw new FormatException("The format of the name is incorrect."); + string tokenId = tokenIDFromName(name); switch (type) { case RecordType.A: @@ -327,11 +322,9 @@ public static void SetRecord(string name, RecordType type, string data) default: throw new InvalidOperationException("The record type is not supported."); } - string tokenId = name[^(fragments[^2].Length + fragments[^1].Length + 1)..]; - ByteString tokenKey = GetKey(tokenId); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); + NameState token = getNameState(nameMap, tokenId); token.CheckAdmin(); + ByteString tokenKey = GetKey(tokenId); byte[] recordKey = GetRecordKey(tokenKey, name, type); recordMap.PutObject(recordKey, new RecordState { @@ -347,12 +340,9 @@ public static string GetRecord(string name, RecordType type) StorageContext context = Storage.CurrentContext; StorageMap nameMap = new(context, Prefix_Name); StorageMap recordMap = new(context, Prefix_Record); - string[] fragments = SplitAndCheck(name, true); - if (fragments is null) throw new FormatException("The format of the name is incorrect."); - string tokenId = name[^(fragments[^2].Length + fragments[^1].Length + 1)..]; + string tokenId = tokenIDFromName(name); + getNameState(nameMap, tokenId); // ensure not expired ByteString tokenKey = GetKey(tokenId); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); byte[] recordKey = GetRecordKey(tokenKey, name, type); RecordState record = (RecordState)recordMap.GetObject(recordKey); if (record is null) return null; @@ -365,10 +355,11 @@ public static Iterator GetAllRecords(string name) StorageContext context = Storage.CurrentContext; StorageMap nameMap = new(context, Prefix_Name); StorageMap recordMap = new(context, Prefix_Record); - ByteString tokenKey = GetKey(name); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); - return (Iterator)recordMap.Find(tokenKey, FindOptions.ValuesOnly | FindOptions.DeserializeValues); + string tokenId = tokenIDFromName(name); + getNameState(nameMap, tokenId); // ensure not expired + ByteString tokenKey = GetKey(tokenId); + byte[] recordsKey = GetRecordsKey(tokenKey, name); + return (Iterator)recordMap.Find(recordsKey, FindOptions.ValuesOnly | FindOptions.DeserializeValues); } public static void DeleteRecord(string name, RecordType type) @@ -376,13 +367,10 @@ public static void DeleteRecord(string name, RecordType type) StorageContext context = Storage.CurrentContext; StorageMap nameMap = new(context, Prefix_Name); StorageMap recordMap = new(context, Prefix_Record); - string[] fragments = SplitAndCheck(name, true); - if (fragments is null) throw new FormatException("The format of the name is incorrect."); - string tokenId = name[^(fragments[^2].Length + fragments[^1].Length + 1)..]; - ByteString tokenKey = GetKey(tokenId); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); + string tokenId = tokenIDFromName(name); + NameState token = getNameState(nameMap, tokenId); token.CheckAdmin(); + ByteString tokenKey = GetKey(tokenId); byte[] recordKey = GetRecordKey(tokenKey, name, type); recordMap.Delete(recordKey); } @@ -417,14 +405,11 @@ private static string Resolve(string name, RecordType type, int redirect) StorageContext context = Storage.CurrentContext; StorageMap nameMap = new(context, Prefix_Name); StorageMap recordMap = new(context, Prefix_Record); - string[] fragments = SplitAndCheck(name, true); - if (fragments is null) throw new FormatException("The format of the name is incorrect."); - string tokenId = name[^(fragments[^2].Length + fragments[^1].Length + 1)..]; + string tokenId = tokenIDFromName(name); + getNameState(nameMap, tokenId); // ensure not expired ByteString tokenKey = GetKey(tokenId); - NameState token = (NameState)StdLib.Deserialize(nameMap[tokenKey]); - token.EnsureNotExpired(); - byte[] recordKey = Helper.Concat((byte[])tokenKey, GetKey(name)); - return (Iterator<(ByteString, RecordState)>)recordMap.Find(recordKey, FindOptions.DeserializeValues); + byte[] recordsKey = GetRecordsKey(tokenKey, name); + return (Iterator<(ByteString, RecordState)>)recordMap.Find(recordsKey, FindOptions.DeserializeValues); } [DisplayName("_deploy")] @@ -458,10 +443,15 @@ private static ByteString GetKey(string tokenId) private static byte[] GetRecordKey(ByteString tokenKey, string name, RecordType type) { - byte[] key = Helper.Concat((byte[])tokenKey, GetKey(name)); + byte[] key = GetRecordsKey(tokenKey, name); return Helper.Concat(key, ((byte)type).ToByteArray()); } + private static byte[] GetRecordsKey(ByteString tokenKey, string name) + { + return Helper.Concat((byte[])tokenKey, GetKey(name)); + } + private static void PostTransfer(UInt160 from, UInt160 to, ByteString tokenId, object data) { OnTransfer(from, to, 1, tokenId); @@ -514,7 +504,7 @@ private static string[] SplitAndCheck(string name, bool allowMultipleFragments) if (length < 3 || length > NameMaxLength) return null; string[] fragments = StdLib.StringSplit(name, "."); length = fragments.Length; - if (length < 2 || length > 8) return null; + if (length > 8) return null; if (length > 2 && !allowMultipleFragments) return null; for (int i = 0; i < length; i++) if (!CheckFragment(fragments[i], i == length - 1)) @@ -615,5 +605,53 @@ private static bool CheckIPv6(string ipv6) } return true; } + + /// + /// Checks provided name for validness and returns corresponding token ID. + /// + private static string tokenIDFromName(string name) + { + string[] fragments = SplitAndCheck(name, true); + if (fragments is null) throw new FormatException("The format of the name is incorrect."); + if (fragments.Length == 1) return name; + return name[^(fragments[^2].Length + fragments[^1].Length + 1)..]; + } + + /// + /// Retrieves NameState from storage and checks that it's not expired as far as the parent domain. + /// + private static NameState getNameState(StorageMap nameMap, string tokenId) + { + ByteString tokenBytes = nameMap[GetKey(tokenId)]; + if (tokenBytes is null) throw new InvalidOperationException("Unknown token."); + NameState token = (NameState)StdLib.Deserialize(tokenBytes); + token.EnsureNotExpired(); + string[] fragments = StdLib.StringSplit(tokenId, "."); + if (parentExpired(nameMap, 1, fragments)) throw new InvalidOperationException("Parent domain has expired."); + return token; + } + + /// + /// Returns true if any domain from fragments doesn't exist or expired. + /// + /// Registered domain names storage map. + /// The deepest subdomain to check. + /// The array of domain name fragments. + /// Whether any domain fragment doesn't exist or expired. + private static bool parentExpired(StorageMap nameMap, int first, string[] fragments) + { + int last = fragments.Length - 1; + string name = fragments[last]; + for (int i = last; i >= first; i--) { + if (i != last) { + name = fragments[i] + "." + name; + } + ByteString buffer = nameMap[GetKey(name)]; + if (buffer is null) return true; + NameState token = (NameState)StdLib.Deserialize(buffer); + if (Runtime.Time >= token.Expiration) return true; + } + return false; + } } }