diff --git a/container/container_contract.go b/container/container_contract.go index 7946923..5c75e33 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" @@ -182,6 +183,7 @@ func PutNamed(container []byte, signature interop.Signature, needRegister bool nnsContractAddr interop.Hash160 domain string + globalDomain string ) if name != "" { if zone == "" { @@ -189,6 +191,12 @@ func PutNamed(container []byte, signature interop.Signature, } nnsContractAddr = storage.Get(ctx, nnsContractKey).(interop.Hash160) domain = name + "." + zone + + tldGlobalDomain := getGlobalDomain(nnsContractAddr, zone) + if tldGlobalDomain != "" { + globalDomain = name + "." + tldGlobalDomain + checkGlobalDomainAvailable(nnsContractAddr, globalDomain) + } needRegister = checkNiceNameAvailable(nnsContractAddr, domain) } @@ -243,10 +251,81 @@ func PutNamed(container []byte, signature interop.Signature, storage.Put(ctx, key, domain) } + if globalDomain != "" { + if res := contract.Call(nnsContractAddr, "register", contract.All, + globalDomain, runtime.GetExecutingScriptHash(), "ops@frostfs.info", + defaultRefresh, defaultRetry, defaultExpire, defaultTTL).(bool); !res { + panic("can't register the global domain") + } + + contract.Call(nnsContractAddr, "addRecord", contract.All, + domain, int64(nns.CNAME), globalDomain) + } + runtime.Log("added new container") runtime.Notify("PutSuccess", containerID, publicKey) } +// solveCnametgt determines the value of the txt parameter for the Cnametgt record of the specified domain. +// If the txt record is missing, the function returns an empty string. +func solveCnametgt(nnsContractAddr interop.Hash160, domain string) string { + if domain == "" { + return "" + } + + res := contract.Call(nnsContractAddr, "getRecords", + contract.ReadStates|contract.AllowCall, domain, nns.TXT) + if res == nil { + return "" + } + + names := res.([]string) + + tgt := "" + for _, name := range names { + fragments := std.StringSplit(name, "=") + if len(fragments) != 2 { + continue + } + + if fragments[0] == nns.Cnametgt { + tgt = fragments[1] + } + } + return tgt +} + +// splitDomain splits domain name into parts. +func splitDomain(name string) []string { + fragments := std.StringSplit(name, ".") + return fragments +} + +// getGlobalDomain returns the GlobalDomain for a specific domain. +func getGlobalDomain(nnsContractAddr interop.Hash160, zone string) string { + subDomains := splitDomain(zone) + globalDomain := "" + if len(subDomains) > 0 { + ns := subDomains[len(subDomains)-1] + if ns == "ns" && len(ns) > 1 { + ns = subDomains[len(subDomains)-2] + "." + subDomains[len(subDomains)-1] + } + if ns != "" { + globalDomain = solveCnametgt(nnsContractAddr, ns) + } + } + return globalDomain +} + +// checkGlobalDomainAvailable checks if the nice name is available for the container. +// It panics if the name is taken. +func checkGlobalDomainAvailable(nnsContractAddr interop.Hash160, globalDomain string) { + if isAvail := contract.Call(nnsContractAddr, "isAvailable", + contract.ReadStates|contract.AllowCall, globalDomain).(bool); !isAvail { + panic("global domain is already taken") + } +} + // checkNiceNameAvailable checks if the nice name is available for the container. // It panics if the name is taken. Returned value specifies if new domain registration is needed. func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool { @@ -301,6 +380,11 @@ func Delete(containerID []byte, signature interop.Signature, publicKey interop.P if res != nil && std.Base58Encode(containerID) == string(res.([]any)[0].(string)) { contract.Call(nnsContractAddr, "deleteRecords", contract.All, domain, 16 /* TXT */) } + + // res = contract.Call(nnsContractAddr, "getRecords", contract.ReadStates|contract.AllowCall, domain, nns.CNAME) + // if res != nil && std.Base58Encode(containerID) == string(res.([]any)[0].(string)) { + // contract.Call(nnsContractAddr, "deleteRecords", contract.All, domain, nns.CNAME) + // } } removeContainer(ctx, containerID, ownerID) runtime.Log("remove container") diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 2eb53c3..4a3770c 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -69,6 +69,10 @@ const ( errInvalidDomainName = "invalid domain name format" ) +const ( + Cnametgt = "cnametgt" +) + // RecordState is a type that registered entities are saved to. type RecordState struct { Name string diff --git a/tests/container_test.go b/tests/container_test.go index 035910e..4ea2c72 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,57 @@ 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...) + }) + t.Run("gas costs are the same for all containers in block", func(t *testing.T) { const ( containerPerBlock = 512