diff --git a/container/config.yml b/container/config.yml index 026efa88..7e52be76 100644 --- a/container/config.yml +++ b/container/config.yml @@ -2,7 +2,7 @@ name: "NeoFS Container" safemethods: ["count", "containersOf", "get", "owner", "list", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "iterateAllContainerSizes", "version"] permissions: - methods: ["update", "addKey", "transferX", - "register", "addRecord", "deleteRecords"] + "register", "registerTLD", "addRecord", "deleteRecords"] events: - name: PutSuccess parameters: diff --git a/container/container_contract.go b/container/container_contract.go index 95ec613d..7da1e2f5 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -208,12 +208,9 @@ func registerNiceNameTLD(addrNNS interop.Hash160, nnsRoot string) { return } - res := contract.Call(addrNNS, "register", contract.All, - nnsRoot, runtime.GetExecutingScriptHash(), "ops@nspcc.ru", - defaultRefresh, defaultRetry, defaultExpire, defaultTTL).(bool) - if !res { - panic("can't register NNS TLD") - } + contract.Call(addrNNS, "registerTLD", contract.All, + nnsRoot, "ops@nspcc.ru", + defaultRefresh, defaultRetry, defaultExpire, defaultTTL) } // Update method updates contract source code and manifest. It can be invoked diff --git a/nns/namestate.go b/nns/namestate.go index 72bbf944..b9f964ca 100644 --- a/nns/namestate.go +++ b/nns/namestate.go @@ -7,6 +7,7 @@ import ( // NameState represents domain name state. type NameState struct { + // Domain name owner. Nil if owned by the committee. Owner interop.Hash160 Name string Expiration int64 @@ -22,6 +23,10 @@ func (n NameState) ensureNotExpired() { // checkAdmin panics if script container is not signed by the domain name admin. func (n NameState) checkAdmin() { + if len(n.Owner) == 0 { + checkCommittee() + return + } if runtime.CheckWitness(n.Owner) { return } diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 46680e5d..6554f8e2 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -100,6 +100,26 @@ func _deploy(data interface{}, isUpdate bool) { ctx := storage.GetContext() storage.Put(ctx, []byte{prefixTotalSupply}, 0) storage.Put(ctx, []byte{prefixRegisterPrice}, defaultRegisterPrice) + + if data != nil { // for backward compatibility + args := data.(struct { + tldSet []struct { + name string + email string + } + }) + + for i := range args.tldSet { + const ( + refresh = 3600 + retry = 600 + expire = 10 * 365 * 24 * 60 * 60 // 10 years + ttl = 3600 + ) + saveCommitteeDomain(ctx, args.tldSet[i].name, args.tldSet[i].email, refresh, retry, expire, ttl) + runtime.Log("registered committee domain " + args.tldSet[i].name) + } + } } // Symbol returns NeoNameService symbol. @@ -123,17 +143,29 @@ func TotalSupply() int { return getTotalSupply(ctx) } -// OwnerOf returns the owner of the specified domain. +// OwnerOf returns the owner of the specified domain. The tokenID domain MUST +// NOT be a TLD. func OwnerOf(tokenID []byte) interop.Hash160 { + fragments := std.StringSplit(string(tokenID), ".") + if len(fragments) == 1 { + panic("token not found") + } + ctx := storage.GetReadOnlyContext() - ns := getNameState(ctx, tokenID) + ns := getFragmentedNameState(ctx, tokenID, fragments) return ns.Owner } // Properties returns a domain name and an expiration date of the specified domain. +// The tokenID MUST NOT be a TLD. func Properties(tokenID []byte) map[string]interface{} { + fragments := std.StringSplit(string(tokenID), ".") + if len(fragments) == 1 { + panic("token not found") + } + ctx := storage.GetReadOnlyContext() - ns := getNameState(ctx, tokenID) + ns := getFragmentedNameState(ctx, tokenID, fragments) return map[string]interface{}{ "name": ns.Name, "expiration": ns.Expiration, @@ -169,10 +201,17 @@ func TokensOf(owner interop.Hash160) iterator.Iterator { } // Transfer transfers the domain with the specified name to a new owner. +// The tokenID MUST NOT be a TLD. func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool { if !isValid(to) { panic(`invalid receiver`) } + + fragments := std.StringSplit(string(tokenID), ".") + if len(fragments) == 1 { + panic("token not found") + } + var ( tokenKey = getTokenKey(tokenID) ctx = storage.GetContext() @@ -220,7 +259,8 @@ func GetPrice() int { return storage.Get(ctx, []byte{prefixRegisterPrice}).(int) } -// IsAvailable checks whether the provided domain name is available. +// IsAvailable checks whether the provided domain name is available. Notice that +// TLD is available for the committee only. func IsAvailable(name string) bool { fragments := splitAndCheck(name, false) if fragments == nil { @@ -259,7 +299,14 @@ func parentExpired(ctx storage.Context, first int, fragments []string) bool { return false } -// Register registers a new domain with the specified owner and name if it's available. +// Register registers a new domain with the specified owner and name if it's +// available. Top-level domains MUST NOT be registered via Register, use +// RegisterTLD for this. +// +// Access rules:: +// - 2nd-level domain can be registered by anyone +// - starting from the 3rd level, the domain can only be registered by the +// owner or administrator (if any) of the previous level domain func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool { fragments := splitAndCheck(name, true) if fragments == nil { @@ -267,36 +314,34 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, } l := len(fragments) - tldKey := append([]byte{prefixRoot}, []byte(fragments[l-1])...) - ctx := storage.GetContext() - tldBytes := storage.Get(ctx, tldKey) if l == 1 { - checkCommittee() - if tldBytes != nil { - panic("TLD already exists") - } - storage.Put(ctx, tldKey, 0) - } else { - if tldBytes == nil { - panic("TLD not found") - } - if parentExpired(ctx, 1, fragments) { - panic("one of the parent domains is not registered") - } - parentKey := getTokenKey([]byte(name[len(fragments[0])+1:])) + panic("TLD denied") + } + + ctx := storage.GetContext() + + if storage.Get(ctx, makeTLDKey(fragments[l-1])) == nil { + panic("TLD not found") + } + if parentExpired(ctx, 1, fragments) { + panic("one of the parent domains is not registered") + } + parentKey := getTokenKey([]byte(name[len(fragments[0])+1:])) + + if l > 2 { nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...)) ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns.checkAdmin() + } - parentRecKey := append([]byte{prefixRecord}, parentKey...) - it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues) - suffix := []byte(name) - for iterator.Next(it) { - r := iterator.Value(it).(RecordState) - ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name)) - if ind > 0 && ind+len(suffix) == len(r.Name) { - panic("parent domain has conflicting records: " + r.Name) - } + parentRecKey := append([]byte{prefixRecord}, parentKey...) + it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues) + suffix := []byte(name) + for iterator.Next(it) { + r := iterator.Value(it).(RecordState) + ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name)) + if ind > 0 && ind+len(suffix) == len(r.Name) { + panic("parent domain has conflicting records: " + r.Name) } } @@ -320,17 +365,54 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, } else { updateTotalSupply(ctx, +1) } - ns := NameState{ + saveDomain(ctx, name, email, refresh, retry, expire, ttl, owner) + updateBalance(ctx, []byte(name), owner, +1) + postTransfer(oldOwner, owner, []byte(name), nil) + return true +} + +// RegisterTLD registers new top-level domain. RegisterTLD MUST be called by the +// committee only. Name MUST be a valid TLD. +// +// RegisterTLD panics with 'TLD already exists' if domain already exists. +func RegisterTLD(name, email string, refresh, retry, expire, ttl int) { + checkCommittee() + saveCommitteeDomain(storage.GetContext(), name, email, refresh, retry, expire, ttl) +} + +// saveCommitteeDomain marks TLD as registered via prefixRoot storage +// record and saves domain state calling saveDomain with given parameters and +// empty owner. The name MUST be a valid TLD name. +func saveCommitteeDomain(ctx storage.Context, name, email string, refresh, retry, expire, ttl int) { + fragments := splitAndCheck(name, false) + if len(fragments) != 1 { + panic("invalid domain name format") + } + + tldKey := makeTLDKey(name) + if storage.Get(ctx, tldKey) != nil { + panic("TLD already exists") + } + + storage.Put(ctx, tldKey, 0) + + var committeeOwner interop.Hash160 + saveDomain(ctx, name, email, refresh, retry, expire, ttl, committeeOwner) +} + +// saveDomain constructs NameState and RecordState of SOA type for the domain +// based on parameters and saves these descriptors in the contract storage. +// Empty owner parameter corresponds to owner-by-committee domains. +// +// Provided storage.Context MUST be read-write. +func saveDomain(ctx storage.Context, name, email string, refresh, retry, expire, ttl int, owner interop.Hash160) { + putNameStateWithKey(ctx, getTokenKey([]byte(name)), NameState{ Owner: owner, Name: name, // NNS expiration is in milliseconds Expiration: int64(runtime.GetTime() + expire*1000), - } - putNameStateWithKey(ctx, tokenKey, ns) + }) putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) - updateBalance(ctx, []byte(name), owner, +1) - postTransfer(oldOwner, owner, []byte(name), nil) - return true } // Renew increases domain expiration date. @@ -358,29 +440,42 @@ func UpdateSOA(name, email string, refresh, retry, expire, ttl int) { putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) } -// SetAdmin updates domain admin. +// SetAdmin updates domain admin. The name MUST NOT be a TLD. func SetAdmin(name string, admin interop.Hash160) { if len(name) > maxDomainNameLength { panic("invalid domain name format") } + + fragments := std.StringSplit(name, ".") + if len(fragments) == 1 { + panic("token not found") + } + if admin != nil && !runtime.CheckWitness(admin) { panic("not witnessed by admin") } ctx := storage.GetContext() - ns := getNameState(ctx, []byte(name)) + ns := getFragmentedNameState(ctx, []byte(name), fragments) common.CheckOwnerWitness(ns.Owner) ns.Admin = admin putNameState(ctx, ns) } // SetRecord adds a new record of the specified type to the provided domain. +// The name MUST NOT be a TLD. func SetRecord(name string, typ RecordType, id byte, data string) { tokenID := []byte(tokenIDFromName(name)) if !checkBaseRecords(typ, data) { panic("invalid record data") } + + fragments := std.StringSplit(name, ".") + if len(fragments) == 1 { + panic("token not found") + } + ctx := storage.GetContext() - ns := getNameState(ctx, tokenID) + ns := getFragmentedNameState(ctx, tokenID, fragments) ns.checkAdmin() putRecord(ctx, tokenID, name, typ, id, data) updateSoaSerial(ctx, tokenID) @@ -402,35 +497,55 @@ func checkBaseRecords(typ RecordType, data string) bool { } // AddRecord adds a new record of the specified type to the provided domain. +// The name MUST NOT be a TLD. func AddRecord(name string, typ RecordType, data string) { tokenID := []byte(tokenIDFromName(name)) if !checkBaseRecords(typ, data) { panic("invalid record data") } + + fragments := std.StringSplit(string(tokenID), ".") + if len(fragments) == 1 { + panic("token not found") + } + ctx := storage.GetContext() - ns := getNameState(ctx, tokenID) + ns := getFragmentedNameState(ctx, tokenID, fragments) ns.checkAdmin() addRecord(ctx, tokenID, name, typ, data) updateSoaSerial(ctx, tokenID) } // GetRecords returns domain record of the specified type if it exists or an empty -// string if not. +// string if not. The name MUST NOT be a TLD. func GetRecords(name string, typ RecordType) []string { + fragments := std.StringSplit(name, ".") + if len(fragments) == 1 { + panic("token not found") + } + tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() - _ = getNameState(ctx, tokenID) // ensure not expired + _ = getFragmentedNameState(ctx, tokenID, fragments) // ensure not expired return getRecordsByType(ctx, tokenID, name, typ) } -// DeleteRecords removes domain records with the specified type. +// DeleteRecords removes domain records with the specified type. The name MUST +// NOT be a TLD. func DeleteRecords(name string, typ RecordType) { if typ == SOA { panic("you cannot delete soa record") } + tokenID := []byte(tokenIDFromName(name)) + + fragments := std.StringSplit(string(tokenID), ".") + if len(fragments) == 1 { + panic("token not found") + } + ctx := storage.GetContext() - ns := getNameState(ctx, tokenID) + ns := getFragmentedNameState(ctx, tokenID, fragments) ns.checkAdmin() recordsKey := getRecordsKeyByType(tokenID, name, typ) records := storage.Find(ctx, recordsKey, storage.KeysOnly) @@ -442,16 +557,28 @@ func DeleteRecords(name string, typ RecordType) { } // Resolve resolves given name (not more then three redirects are allowed). +// The name MUST NOT be a TLD. func Resolve(name string, typ RecordType) []string { + fragments := std.StringSplit(name, ".") + if len(fragments) == 1 { + panic("token not found") + } + ctx := storage.GetReadOnlyContext() return resolve(ctx, nil, name, typ, 2) } // GetAllRecords returns an Iterator with RecordState items for the given name. +// The name MUST NOT be a TLD. func GetAllRecords(name string) iterator.Iterator { + fragments := std.StringSplit(name, ".") + if len(fragments) == 1 { + panic("token not found") + } + tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() - _ = getNameState(ctx, tokenID) // ensure not expired + _ = getFragmentedNameState(ctx, tokenID, fragments) // ensure not expired recordsKey := getRecordsKey(tokenID, name) return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) } @@ -508,9 +635,18 @@ func getTokenKey(tokenID []byte) []byte { // getNameState returns domain name state by the specified tokenID. func getNameState(ctx storage.Context, tokenID []byte) NameState { + return getFragmentedNameState(ctx, tokenID, nil) +} + +// getFragmentedNameState returns domain name state by the specified tokenID. +// Optional fragments parameter allows to pass pre-calculated elements of the +// domain name path: if empty, getFragmentedNameState splits name on its own. +func getFragmentedNameState(ctx storage.Context, tokenID []byte, fragments []string) NameState { tokenKey := getTokenKey(tokenID) ns := getNameStateWithKey(ctx, tokenKey) - fragments := std.StringSplit(string(tokenID), ".") + if len(fragments) == 0 { + fragments = std.StringSplit(string(tokenID), ".") + } if parentExpired(ctx, 1, fragments) { panic("parent domain has expired") } @@ -911,3 +1047,7 @@ func getAllRecords(ctx storage.Context, name string) iterator.Iterator { recordsKey := getRecordsKey(tokenID, name) return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) } + +func makeTLDKey(name string) []byte { + return append([]byte{prefixRoot}, name...) +} diff --git a/tests/container_test.go b/tests/container_test.go index 8e6cd8ba..6bf3d21c 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -208,9 +208,8 @@ func TestContainerPut(t *testing.T) { cnt.value[len(cnt.value)-1] = 10 cnt.id = sha256.Sum256(cnt.value) - cNNS.Invoke(t, true, "register", - "cdn", c.CommitteeHash, - "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0)) + cNNS.Invoke(t, stackitem.Null{}, "registerTLD", + "cdn", "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0)) cNNS.Invoke(t, true, "register", "domain.cdn", c.CommitteeHash, diff --git a/tests/migration/storage.go b/tests/migration/storage.go index 97b9a457..0d663344 100644 --- a/tests/migration/storage.go +++ b/tests/migration/storage.go @@ -152,7 +152,9 @@ func NewContract(tb testing.TB, d *dump.Reader, name string, opts ContractOption const nnsSourceCodeDir = "../nns" exec.DeployContract(tb, neotest.CompileFile(tb, exec.CommitteeHash, nnsSourceCodeDir, filepath.Join(nnsSourceCodeDir, "config.yml")), - []interface{}{}, + []interface{}{ + []interface{}{[]interface{}{"neofs", "ops@morphbits.io"}}, + }, ) // compile new contract version @@ -244,16 +246,6 @@ func (x *Contract) GetStorageItem(key []byte) []byte { // See also nns.Register, nns.AddRecord. func (x *Contract) RegisterContractInNNS(tb testing.TB, name string, addr util.Uint160) { nnsInvoker := x.exec.CommitteeInvoker(x.exec.ContractHash(tb, 1)) - nnsInvoker.InvokeAndCheck(tb, checkSingleTrueInStack, "register", - "neofs", - x.exec.CommitteeHash, - "ops@morphbits.io", - int64(3600), - int64(600), - int64(10*365*24*time.Hour/time.Second), - int64(3600), - ) - domain := name + ".neofs" nnsInvoker.InvokeAndCheck(tb, checkSingleTrueInStack, "register", diff --git a/tests/nns_test.go b/tests/nns_test.go index 2f8049ca..27267a3f 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -10,7 +10,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neofs-contract/common" "github.com/nspcc-dev/neofs-contract/nns" "github.com/stretchr/testify/require" ) @@ -19,18 +21,25 @@ const nnsPath = "../nns" const msPerYear = 365 * 24 * time.Hour / time.Millisecond -func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker { +func newNNSInvoker(t *testing.T, addRoot bool, tldSet ...string) *neotest.ContractInvoker { e := newExecutor(t) ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) - e.DeployContract(t, ctr, nil) + if len(tldSet) > 0 { + _tldSet := make([]interface{}, len(tldSet)) + for i := range tldSet { + _tldSet[i] = []interface{}{tldSet[i], "user@domain.org"} + } + e.DeployContract(t, ctr, []interface{}{_tldSet}) + } else { + e.DeployContract(t, ctr, nil) + } c := e.CommitteeInvoker(ctr.Hash) if addRoot { // Set expiration big enough to pass all tests. refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*100), int64(104) - c.Invoke(t, true, "register", - "com", c.CommitteeHash, - "myemail@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, stackitem.Null{}, "registerTLD", + "com", "myemail@nspcc.ru", refresh, retry, expire, ttl) } return c } @@ -48,23 +57,19 @@ func TestNNSRegisterTLD(t *testing.T) { refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) - c.InvokeFail(t, "invalid domain name format", "register", - "0com", c.CommitteeHash, - "email@nspcc.ru", refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "registerTLD", + "0com", "email@nspcc.ru", refresh, retry, expire, ttl) acc := c.NewAccount(t) cAcc := c.WithSigners(acc) - cAcc.InvokeFail(t, "not witnessed by committee", "register", - "com", acc.ScriptHash(), - "email@nspcc.ru", refresh, retry, expire, ttl) + cAcc.InvokeFail(t, "not witnessed by committee", "registerTLD", + "com", "email@nspcc.ru", refresh, retry, expire, ttl) - c.Invoke(t, true, "register", - "com", c.CommitteeHash, - "email@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, stackitem.Null{}, "registerTLD", + "com", "email@nspcc.ru", refresh, retry, expire, ttl) - c.InvokeFail(t, "TLD already exists", "register", - "com", c.CommitteeHash, - "email@nspcc.ru", refresh, retry, expire, ttl) + c.InvokeFail(t, "TLD already exists", "registerTLD", + "com", "email@nspcc.ru", refresh, retry, expire, ttl) } func TestNNSRegister(t *testing.T) { @@ -73,15 +78,10 @@ func TestNNSRegister(t *testing.T) { accTop := c.NewAccount(t) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) c1 := c.WithSigners(c.Committee, accTop) - c1.Invoke(t, true, "register", - "com", accTop.ScriptHash(), - "myemail@nspcc.ru", refresh, retry, expire, ttl) + c1.Invoke(t, stackitem.Null{}, "registerTLD", + "com", "myemail@nspcc.ru", refresh, retry, expire, ttl) acc := c.NewAccount(t) - c2 := c.WithSigners(c.Committee, acc) - c2.InvokeFail(t, "not witnessed by admin", "register", - "testdomain.com", acc.ScriptHash(), - "myemail@nspcc.ru", refresh, retry, expire, ttl) c3 := c.WithSigners(accTop, acc) t.Run("domain names with hyphen", func(t *testing.T) { @@ -127,15 +127,6 @@ func TestNNSRegister(t *testing.T) { c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT)) } -func TestTLDRecord(t *testing.T) { - c := newNNSInvoker(t, true) - c.Invoke(t, stackitem.Null{}, "addRecord", - "com", int64(nns.A), "1.2.3.4") - - result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))} - c.Invoke(t, result, "resolve", "com", int64(nns.A)) -} - func TestNNSRegisterMulti(t *testing.T) { c := newNNSInvoker(t, true) @@ -316,9 +307,8 @@ func TestNNSIsAvailable(t *testing.T) { c.InvokeFail(t, "TLD not found", "isAvailable", "domain.com") refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) - c.Invoke(t, true, "register", - "com", c.CommitteeHash, - "myemail@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, stackitem.Null{}, "registerTLD", + "com", "myemail@nspcc.ru", refresh, retry, expire, ttl) c.Invoke(t, false, "isAvailable", "com") c.Invoke(t, true, "isAvailable", "domain.com") @@ -371,3 +361,119 @@ func TestNNSResolve(t *testing.T) { c.Invoke(t, records, "resolve", "test.com.", int64(nns.TXT)) c.InvokeFail(t, "invalid domain name format", "resolve", "test.com..", int64(nns.TXT)) } + +func TestNNSRegisterAccess(t *testing.T) { + inv := newNNSInvoker(t, false) + const email, refresh, retry, expire, ttl = "user@domain.org", 0, 1, 2, 3 + const tld = "com" + const registerMethod = "register" + const registerTLDMethod = "registerTLD" + const tldDeniedFailMsg = "TLD denied" + + // TLD + l2OwnerAcc := inv.NewAccount(t) + l2OwnerInv := inv.WithSigners(l2OwnerAcc) + + l2OwnerInv.InvokeFail(t, tldDeniedFailMsg, registerMethod, + tld, l2OwnerAcc.ScriptHash(), email, refresh, retry, expire, ttl) + l2OwnerInv.InvokeFail(t, tldDeniedFailMsg, registerMethod, + tld, nil, email, refresh, retry, expire, ttl) + + inv.WithSigners(inv.Committee).InvokeFail(t, tldDeniedFailMsg, registerMethod, + tld, nil, email, refresh, retry, expire, ttl) + inv.WithSigners(inv.Committee).Invoke(t, stackitem.Null{}, registerTLDMethod, + tld, email, refresh, retry, expire, ttl) + + // L2 + const l2 = "l2." + tld + + anonymousAcc := inv.NewAccount(t) + anonymousInv := inv.WithSigners(anonymousAcc) + + l2OwnerInv.InvokeFail(t, "invalid owner", registerMethod, + l2, nil, email, refresh, retry, expire, ttl) + l2OwnerInv.InvokeFail(t, common.ErrOwnerWitnessFailed, registerMethod, + l2, anonymousAcc.ScriptHash(), email, refresh, retry, expire, ttl) + l2OwnerInv.Invoke(t, true, registerMethod, + l2, l2OwnerAcc.ScriptHash(), email, refresh, retry, expire, ttl) + + // L3 (by L2 owner) + const l3ByL2Owner = "l3-owner." + l2 + + l2OwnerInv.Invoke(t, true, registerMethod, + l3ByL2Owner, l2OwnerAcc.ScriptHash(), email, refresh, retry, expire, ttl) + + // L3 (by L2 admin) + const l3ByL2Admin = "l3-admin." + l2 + + l2AdminAcc := inv.NewAccount(t) + l2AdminInv := inv.WithSigners(l2AdminAcc) + + inv.WithSigners(l2OwnerAcc, l2AdminAcc).Invoke(t, stackitem.Null{}, "setAdmin", l2, l2AdminAcc.ScriptHash()) + + anonymousInv.InvokeFail(t, "not witnessed by admin", registerMethod, + l3ByL2Admin, anonymousAcc.ScriptHash(), email, refresh, retry, expire, ttl) + l2AdminInv.Invoke(t, true, "register", + l3ByL2Admin, l2AdminAcc.ScriptHash(), email, refresh, retry, expire, ttl) +} + +func TestPredefinedTLD(t *testing.T) { + predefined := []string{"hello", "world"} + const otherTLD = "goodbye" + + inv := newNNSInvoker(t, false, predefined...) + + inv.Invoke(t, true, "isAvailable", otherTLD) + + for i := range predefined { + inv.Invoke(t, false, "isAvailable", predefined[i]) + } +} + +func TestNNSTLD(t *testing.T) { + const tld = "any-tld" + const tldFailMsg = "token not found" + const recTyp = int64(nns.TXT) // InvokeFail doesn't support nns.RecordType + + inv := newNNSInvoker(t, false, tld) + + inv.InvokeFail(t, tldFailMsg, "addRecord", tld, recTyp, "any data") + inv.InvokeFail(t, tldFailMsg, "deleteRecords", tld, recTyp) + inv.InvokeFail(t, tldFailMsg, "getAllRecords", tld) + inv.InvokeFail(t, tldFailMsg, "getRecords", tld, recTyp) + inv.Invoke(t, false, "isAvailable", tld) + inv.InvokeFail(t, tldFailMsg, "ownerOf", tld) + inv.InvokeFail(t, tldFailMsg, "properties", tld) + inv.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {}, "renew", tld) + inv.InvokeFail(t, tldFailMsg, "resolve", tld, recTyp) + inv.InvokeFail(t, tldFailMsg, "setAdmin", tld, util.Uint160{}) + inv.InvokeFail(t, tldFailMsg, "setRecord", tld, recTyp, 1, "any data") + inv.InvokeFail(t, tldFailMsg, "transfer", util.Uint160{}, tld, nil) + inv.Invoke(t, stackitem.Null{}, "updateSOA", tld, "user@domain.org", 0, 1, 2, 3) +} + +func TestNNSRoots(t *testing.T) { + tlds := []string{"hello", "world"} + + inv := newNNSInvoker(t, false, tlds...) + + stack, err := inv.TestInvoke(t, "roots") + require.NoError(t, err) + require.NotEmpty(t, stack) + + it, ok := stack.Pop().Value().(*storage.Iterator) + require.True(t, ok) + + var res []string + + for it.Next() { + item := it.Value() + + b, err := item.TryBytes() + require.NoError(t, err) + + res = append(res, string(b)) + } + + require.ElementsMatch(t, tlds, res) +}