Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NameService: minor improvements, part 2 #25

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 84 additions & 43 deletions src/NameService/NameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,15 @@ 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;
}

[Safe]
public static Map<string, object> 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<string, object> map = new();
map["name"] = token.Name;
map["expiration"] = token.Expiration;
Expand Down Expand Up @@ -200,10 +198,7 @@ public static bool IsAvailable(string name)
if (rootMap[fragments[^1]] is null) throw new Exception("The root does not exist.");
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)
Expand All @@ -216,6 +211,10 @@ public static bool Register(string name, UInt160 owner)
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 (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)
Expand Down Expand Up @@ -285,13 +284,12 @@ 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);
ulong oldExpiration = token.Expiration;
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);
OnRenew(name, oldExpiration, token.Expiration);
return token.Expiration;
Expand All @@ -302,12 +300,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);
}
Expand All @@ -317,8 +314,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:
Expand All @@ -336,11 +332,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
{
Expand All @@ -356,12 +350,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;
Expand All @@ -374,24 +365,22 @@ public static Iterator<RecordState> 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<RecordState>)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<RecordState>)recordMap.Find(recordsKey, FindOptions.ValuesOnly | FindOptions.DeserializeValues);
}

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);
}
Expand Down Expand Up @@ -426,14 +415,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")]
Expand Down Expand Up @@ -491,10 +477,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);
Expand Down Expand Up @@ -648,5 +639,55 @@ private static bool CheckIPv6(string ipv6)
}
return true;
}

/// <summary>
/// Checks provided name for validness and returns corresponding token ID.
/// </summary>
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)..];
}

/// <summary>
/// Retrieves NameState from storage and checks that it's not expired as far as the parent domain.
/// </summary>
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;
}

/// <summary>
/// Returns true if any domain from fragments doesn't exist or expired.
/// </summary>
/// <param name="nameMap">Registered domain names storage map.</param>
/// <param name="first">The deepest subdomain to check.</param>
/// <param name="fragments">The array of domain name fragments.</param>
/// <returns>Whether any domain fragment doesn't exist or expired.</returns>
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not better to store the min expiration time into NameState, seems expensive to me the method getNameState

Copy link
Member Author

@AnnaShaleva AnnaShaleva Sep 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then on every Renew we have to iterate over the whole set of dependant token children and re-calculate minimum expiration time for each NameState which implies retrieving the whole set of parent domains. It can be rather expensive too.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect there to be ~2-3 iterations typically which doesn't seem to be a lot. Many of the affected methods are safe (OwnerOf, Properties, IsAvailable, etc.), so they'll be mostly invoked in test mode, not on-chain. Non-safe affected methods can have more problems with the alternative layout as with the Renew case above.

}
return false;
}
}
}