[#102] nns: Add support global domain
Signed-off-by: Alexander Chuprov <a.chuprov@yadro.com>
This commit is contained in:
parent
49e5270f67
commit
a0388358df
6 changed files with 222 additions and 1 deletions
|
@ -18,6 +18,8 @@ permissions:
|
|||
- "register"
|
||||
- "transferX"
|
||||
- "update"
|
||||
- "getGlobalDomain"
|
||||
- "deleteDomain"
|
||||
|
||||
events:
|
||||
- name: PutSuccess
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue