[#102] nns: Add support global domain
All checks were successful
DCO action / DCO (pull_request) Successful in 40s
Tests / Tests (1.21) (pull_request) Successful in 1m6s
Tests / Tests (1.22) (pull_request) Successful in 1m7s

Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
This commit is contained in:
Alexander Chuprov 2024-08-02 21:38:49 +03:00
parent 49e5270f67
commit a0388358df
6 changed files with 222 additions and 1 deletions

View file

@ -18,6 +18,8 @@ permissions:
- "register"
- "transferX"
- "update"
- "getGlobalDomain"
- "deleteDomain"
events:
- name: PutSuccess

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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))
}