From db9cea5ecca92863571b9bf291e1e2ab666b6cdc Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:27:07 +0300 Subject: [PATCH] nns: allow arbitrary level domains Port https://github.com/nspcc-dev/neofs-contract/pull/175/commits/d10da892d9914238ae686e83553ce6539a3b5458. --- examples/nft-nd-nns/nns.go | 42 ++++++++++++++++++++----------- examples/nft-nd-nns/nns_test.go | 44 ++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 948bfa1c4..c3b04931b 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -247,7 +247,7 @@ func parentExpired(ctx storage.Context, first int, fragments []string) bool { // Register registers new domain with the specified owner and name if it's available. func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool { - fragments := splitAndCheck(name, false) + fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } @@ -266,9 +266,9 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, panic("TLD not found") } if parentExpired(ctx, 1, fragments) { - panic("one of the parent domains has expired") + panic("one of the parent domains is not registered") } - parentKey := getTokenKey([]byte(fragments[1])) + parentKey := getTokenKey([]byte(name[len(fragments[0])+1:])) nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...)) ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns.checkAdmin() @@ -331,7 +331,7 @@ func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expir std.Itoa(retry, 10) + " " + std.Itoa(expire, 10) + " " + std.Itoa(ttl, 10) - tokenId := []byte(tokenIDFromName(name)) + tokenId := []byte(tokenIDFromName(ctx, name)) putRecord(ctx, tokenId, name, SOA, 0, data) } @@ -428,7 +428,7 @@ func AddRecord(name string, typ RecordType, data string) { // checkRecord performs record validness check and returns token ID. func checkRecord(ctx storage.Context, name string, typ RecordType, data string) []byte { - tokenID := []byte(tokenIDFromName(name)) + tokenID := []byte(tokenIDFromName(ctx, name)) var ok bool switch typ { case A: @@ -453,8 +453,8 @@ func checkRecord(ctx storage.Context, name string, typ RecordType, data string) // GetRecords returns domain records of the specified type if they exist or an empty // array if not. func GetRecords(name string, typ RecordType) []string { - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() + tokenID := []byte(tokenIDFromName(ctx, name)) _ = getNameState(ctx, tokenID) // ensure not expired return getRecordsByType(ctx, tokenID, name, typ) } @@ -464,8 +464,8 @@ func DeleteRecords(name string, typ RecordType) { if typ == SOA { panic("forbidden to delete SOA record") } - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext() + tokenID := []byte(tokenIDFromName(ctx, name)) ns := getNameState(ctx, tokenID) ns.checkAdmin() recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) @@ -554,7 +554,7 @@ func getNameState(ctx storage.Context, tokenID []byte) NameState { // 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") @@ -564,6 +564,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)) @@ -806,16 +811,25 @@ 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) - if l == 1 { - return name + 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[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):] + return name } // resolve resolves provided name using record with the specified type and given @@ -855,7 +869,7 @@ func resolve(ctx storage.Context, res []string, name string, typ RecordType, red // 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(name)) + 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_test.go b/examples/nft-nd-nns/nns_test.go index e1dd7452d..1441baa2b 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -168,7 +168,7 @@ func TestRegisterAndRenew(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl) c.Invoke(t, true, "isAvailable", "neo.com") c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) @@ -593,6 +593,48 @@ func TestNNSAddRecord(t *testing.T) { } } +func TestNNSRegisterArbitraryLevelDomain(t *testing.T) { + c := newNSClient(t, true) + + newArgs := func(domain string, account neotest.Signer) []interface{} { + return []interface{}{ + domain, account.ScriptHash(), "doesnt@matter.com", + int64(101), int64(102), int64(103), int64(104), + } + } + acc := c.NewAccount(t) + cBoth := c.WithSigners(c.Committee, acc) + args := newArgs("neo.com", acc) + cBoth.Invoke(t, true, "register", args...) + + c1 := c.WithSigners(acc) + // parent domain is missing + args[0] = "testnet.fs.neo.com" + c1.InvokeFail(t, "one of the parent domains is not registered", "register", args...) + + args[0] = "fs.neo.com" + c1.Invoke(t, true, "register", args...) + + args[0] = "testnet.fs.neo.com" + c1.Invoke(t, true, "register", args...) + + acc2 := c.NewAccount(t) + c2 := c.WithSigners(c.Committee, acc2) + args = newArgs("mainnet.fs.neo.com", acc2) + c2.InvokeFail(t, "not witnessed by admin", "register", args...) + + c2 = c.WithSigners(acc, acc2) + c2.Invoke(t, true, "register", args...) + + c2 = c.WithSigners(acc2) + c2.Invoke(t, stackitem.Null{}, "addRecord", + "cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13") + result := stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("166.15.14.13")), + }) + c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A)) +} + const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000