From a0388358dfe019f101a57fe45acf11f4d1538d29 Mon Sep 17 00:00:00 2001 From: Alexander Chuprov Date: Fri, 2 Aug 2024 21:38:49 +0300 Subject: [PATCH] [#102] nns: Add support global domain Signed-off-by: Alexander Chuprov --- container/config.yml | 2 + container/container_contract.go | 8 +++ nns/config.yml | 2 +- nns/nns_contract.go | 120 ++++++++++++++++++++++++++++++++ tests/container_test.go | 64 +++++++++++++++++ tests/nns_test.go | 27 +++++++ 6 files changed, 222 insertions(+), 1 deletion(-) diff --git a/container/config.yml b/container/config.yml index c040d12..21bbdc7 100644 --- a/container/config.yml +++ b/container/config.yml @@ -18,6 +18,8 @@ permissions: - "register" - "transferX" - "update" + - "getGlobalDomain" + - "deleteDomain" events: - name: PutSuccess diff --git a/container/container_contract.go b/container/container_contract.go index 7946923..bf04d0f 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -2,6 +2,7 @@ package container import ( "git.frostfs.info/TrueCloudLab/frostfs-contract/common" + "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/contract" "github.com/nspcc-dev/neo-go/pkg/interop/convert" @@ -300,6 +301,13 @@ func Delete(containerID []byte, signature interop.Signature, publicKey interop.P res := contract.Call(nnsContractAddr, "getRecords", contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */) if res != nil && std.Base58Encode(containerID) == string(res.([]any)[0].(string)) { contract.Call(nnsContractAddr, "deleteRecords", contract.All, domain, 16 /* TXT */) + res1 := contract.Call(nnsContractAddr, "getGlobalDomain", contract.All, domain) + + if res1 != nil && res1.(string) != "" { + globalDomain := res1.(string) + contract.Call(nnsContractAddr, "deleteRecords", contract.All, globalDomain, nns.CNAME) + contract.Call(nnsContractAddr, "deleteDomain", contract.All, globalDomain) + } } } removeContainer(ctx, containerID, ownerID) diff --git a/nns/config.yml b/nns/config.yml index 75a111d..fc3c679 100644 --- a/nns/config.yml +++ b/nns/config.yml @@ -2,7 +2,7 @@ name: "NameService" supportedstandards: ["NEP-11"] safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", - "resolve", "version"] + "resolve", "version","getGlobalDomain","checkAvailableGlobalDomain"] events: - name: Transfer parameters: diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 2eb53c3..1730d01 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -69,6 +69,12 @@ const ( errInvalidDomainName = "invalid domain name format" ) +const ( + // Cnametgt is a special TXT record ensuring all created subdomains point to the global domain - the value of this variable. + //It is guaranteed that two domains cannot point to the same global domain. + Cnametgt = "cnametgt" +) + // RecordState is a type that registered entities are saved to. type RecordState struct { Name string @@ -231,9 +237,72 @@ func IsAvailable(name string) bool { return true } checkParentExists(ctx, fragments) + checkAvailableGlobalDomain(ctx, fragments) return storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...)) == nil } +// CheckAvailableGlobalDomain - triggers a panic if the global domain name is occupied. +func CheckAvailableGlobalDomain(name string) { + fragments := splitAndCheck(name) + ctx := storage.GetReadOnlyContext() + checkAvailableGlobalDomain(ctx, fragments) +} + +func checkAvailableGlobalDomain(ctx storage.Context, fragments []string) { + globalDomain := getGlobalDomain(ctx, fragments) + if globalDomain == "" { + return + } + + nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(globalDomain))...)) + if nsBytes != nil { + panic("global domain is already taken") + } +} + +func GetGlobalDomain(name string) string { + fragments := splitAndCheck(name) + ctx := storage.GetReadOnlyContext() + return getGlobalDomain(ctx, fragments) +} + +func getGlobalDomain(ctx storage.Context, fragments []string) string { + name := "" + for i := 1; i < len(fragments); i++ { + if name != "" { + name = name + "." + } + name = name + fragments[i] + } + + if name == "" { + return "" + } + tokenID := []byte(tokenIDFromName(name)) + records := getRecordsByType(ctx, tokenID, name, TXT) + + if records == nil { + return "" + } + globalDomain := "" + for _, name := range records { + fragments := std.StringSplit(name, "=") + if len(fragments) != 2 { + continue + } + + if fragments[0] == Cnametgt { + globalDomain = fragments[1] + } + } + + if globalDomain == "" { + return "" + } + + return fragments[0] + "." + globalDomain +} + // checkParentExists panics if any domain from fragments doesn't exist or is expired. func checkParentExists(ctx storage.Context, fragments []string) { if dom := parentExpired(ctx, fragments); dom != "" { @@ -325,13 +394,49 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, // NNS expiration is in milliseconds Expiration: int64(runtime.GetTime() + expire*1000), } + checkAvailableGlobalDomain(ctx, fragments) + putNameStateWithKey(ctx, tokenKey, ns) putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) updateBalance(ctx, []byte(name), owner, +1) postTransfer(oldOwner, owner, []byte(name), nil) + + registrySubGlobalDomain(ctx, fragments, email, name, refresh, retry, expire, ttl, owner, oldOwner) return true } +func registrySubGlobalDomain(ctx storage.Context, fragments []string, email, originName string, refresh, retry, expire, ttl int, owner, oldOwner interop.Hash160) { + name := getGlobalDomain(ctx, fragments) + if name == "" { + return + } + ns := NameState{ + Owner: owner, + Name: name, + // NNS expiration is in milliseconds + Expiration: int64(runtime.GetTime() + expire*1000), + } + splitAndCheck(name) + tokenKey := getTokenKey([]byte(name)) + putNameStateWithKey(ctx, tokenKey, ns) + putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) + updateBalance(ctx, []byte(name), owner, +1) + postTransfer(oldOwner, owner, []byte(name), nil) + + putCnameRecord(ctx, originName, name) +} + +func DeleteDomain(name string) { + //tokenKey := getTokenKey([]byte(name)) + nameKey := append([]byte{prefixName}, getTokenKey([]byte(name))...) + ctx := storage.GetContext() + tldBytes := storage.Get(ctx, nameKey) + if tldBytes == nil { + return + } + storage.Delete(ctx, nameKey) +} + // Renew increases domain expiration date. func Renew(name string) int64 { checkDomainNameLength(name) @@ -614,6 +719,21 @@ func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expir storage.Put(ctx, recordKey, recBytes) } +// putCnameRecord stores CNAME domain record. +func putCnameRecord(ctx storage.Context, name, data string) { + var id byte + tokenId := []byte(tokenIDFromName(name)) + recordKey := getIdRecordKey(tokenId, name, CNAME, id) + rs := RecordState{ + Name: name, + Type: CNAME, + ID: id, + Data: data, + } + recBytes := std.Serialize(rs) + storage.Put(ctx, recordKey, recBytes) +} + // updateSoaSerial stores soa domain record. func updateSoaSerial(ctx storage.Context, tokenId []byte) { var id byte diff --git a/tests/container_test.go b/tests/container_test.go index 035910e..35bdd20 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -165,6 +165,14 @@ func checkContainerList(t *testing.T, c *neotest.ContractInvoker, expected [][]b }) } +const ( + // default SOA record field values + defaultRefresh = 3600 // 1 hour + defaultRetry = 600 // 10 min + defaultExpire = 3600 * 24 * 365 * 10 // 10 years + defaultTTL = 3600 // 1 hour +) + func TestContainerPut(t *testing.T) { c, cBal, _ := newContainerInvoker(t) @@ -233,6 +241,62 @@ func TestContainerPut(t *testing.T) { }) }) + t.Run("create global domain", func(t *testing.T) { + ctrNNS := neotest.CompileFile(t, c.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) + nnsHash := ctrNNS.Hash + cNNS := c.CommitteeInvoker(nnsHash) + cNNS.Invoke(t, true, "register", + "animals", c.CommitteeHash, + "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0)) + + cNNS.Invoke(t, true, "register", + "ns", c.CommitteeHash, + "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0)) + + cNNS.Invoke(t, true, "register", + "poland.ns", c.CommitteeHash, + "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0)) + + cNNS.Invoke(t, true, "register", + "sweden.ns", c.CommitteeHash, + "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0)) + + cNNS.Invoke(t, stackitem.Null{}, "addRecord", + "poland.ns", int64(nns.TXT), nns.Cnametgt+"=animals") + + cNNS.Invoke(t, stackitem.Null{}, "addRecord", "poland.ns", int64(nns.TXT), "random-record") + + cNNS.Invoke(t, stackitem.Null{}, "addRecord", "poland.ns", int64(nns.TXT), "ne-qqq=random-record2") + cNNS.Invoke(t, stackitem.Null{}, "addRecord", "sweden.ns", int64(nns.TXT), nns.Cnametgt+"=animals") + + balanceMint(t, cBal, acc, (containerFee+containerAliasFee)*5, []byte{}) + putArgs := []any{cnt.value, cnt.sig, cnt.pub, cnt.token, "bober", "poland.ns"} + c3 := c.WithSigners(c.Committee, acc) + c3.Invoke(t, stackitem.Null{}, "putNamed", putArgs...) + b := c3.TopBlock(t) + + expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer( + []byte(fmt.Sprintf("bober.animals ops@frostfs.info %d %d %d %d %d", + b.Timestamp, defaultRefresh, defaultRetry, defaultExpire, defaultTTL)))}) + cNNS.Invoke(t, expected, "getRecords", "bober.animals", int64(nns.SOA)) + + expected = stackitem.NewArray([]stackitem.Item{ + stackitem.NewBuffer([]byte("bober.animals")), + }) + + cNNS.Invoke(t, false, "isAvailable", "bober.animals") + + cNNS.Invoke(t, expected, "getRecords", "bober.poland.ns", int64(nns.CNAME)) + + putArgs = []any{cnt.value, cnt.sig, cnt.pub, cnt.token, "bober", "sweden.ns"} + + c3.InvokeFail(t, "global domain is already taken", "putNamed", putArgs...) + cNNS.InvokeFail(t, "global domain is already taken", "checkAvailableGlobalDomain", "bober.poland.ns") + + c3.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.pub, cnt.token) + cNNS.Invoke(t, stackitem.Null{}, "checkAvailableGlobalDomain", "bober.poland.ns") + }) + t.Run("gas costs are the same for all containers in block", func(t *testing.T) { const ( containerPerBlock = 512 diff --git a/tests/nns_test.go b/tests/nns_test.go index c27d0a1..f5492ab 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -99,6 +99,10 @@ func TestNNSRegister(t *testing.T) { "com", accTop.ScriptHash(), "myemail@frostfs.info", refresh, retry, expire, ttl) + c1.Invoke(t, true, "register", + "globaldomain.com", accTop.ScriptHash(), + "myemail@frostfs.info", refresh, retry, expire, ttl) + acc := c.NewAccount(t) c2 := c.WithSigners(c.Committee, acc) c2.InvokeFail(t, "not witnessed by admin", "register", @@ -157,6 +161,16 @@ func TestNNSRegister(t *testing.T) { stackitem.NewByteArray([]byte("second TXT record")), }) c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT)) + + c3.Invoke(t, stackitem.Null{}, "addRecord", + "testdomain.com", int64(nns.TXT), nns.Cnametgt+"=globaldomain.com") + c.Invoke(t, true, "isAvailable", "dom.testdomain.com") + + c3.Invoke(t, true, "register", + "dom.testdomain.com", acc.ScriptHash(), + "myemail@frostfs.info", refresh, retry, expire, ttl) + + c.Invoke(t, false, "isAvailable", "dom.globaldomain.com") } func TestTLDRecord(t *testing.T) { @@ -365,6 +379,10 @@ func TestNNSIsAvailable(t *testing.T) { "domain.com", acc.ScriptHash(), "myemail@frostfs.info", refresh, retry, expire, ttl) + c1.Invoke(t, true, "register", + "globaldomain.com", acc.ScriptHash(), + "myemail@frostfs.info", refresh, retry, expire, ttl) + c.Invoke(t, false, "isAvailable", "domain.com") c.Invoke(t, true, "isAvailable", "dom.domain.com") @@ -376,6 +394,15 @@ func TestNNSIsAvailable(t *testing.T) { c.Invoke(t, false, "isAvailable", "dom.domain.com") c.Invoke(t, true, "isAvailable", "dom.dom.domain.com") + c1.Invoke(t, stackitem.Null{}, "addRecord", + "dom.domain.com", int64(nns.TXT), nns.Cnametgt+"=globaldomain.com") + c.Invoke(t, true, "isAvailable", "dom.dom.domain.com") + + c1.Invoke(t, true, "register", + "dom.globaldomain.com", acc.ScriptHash(), + "myemail@frostfs.info", refresh, retry, expire, ttl) + c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.dom.domain.com") + c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255)) }