From c9050cef4b6c25675693c0ca9f179a52f0a1c99a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:36:13 +0300 Subject: [PATCH] nns: allow multiple records of the same type Except for the CNAME records. Port https://github.com/nspcc-dev/neofs-contract/pull/133/commits/6ea4573ef86c445709c792f4b40c7ae200e7d799 and https://github.com/nspcc-dev/neofs-contract/pull/133/commits/f4762c1b5643382199fe3795a345ac6ba0cb1727. --- examples/nft-nd-nns/nns.go | 167 +++++++++++++++++++++----------- examples/nft-nd-nns/nns.yml | 2 +- examples/nft-nd-nns/nns_test.go | 160 ++++++++++++++++++++++-------- internal/basicchain/basic.go | 2 +- 4 files changed, 228 insertions(+), 103 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 69ed6d5a6..74ac349ab 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -55,6 +55,9 @@ const ( maxDomainNameLength = 255 // maxTXTRecordLength is the maximum length of the TXT domain record. maxTXTRecordLength = 255 + // maxRecordID is the maximum value of record ID (the upper bound for the number + // of records with the same type). + maxRecordID = 255 ) // Other constants. @@ -70,6 +73,7 @@ type RecordState struct { Name string Type RecordType Data string + ID byte } // Update updates NameService contract. @@ -337,8 +341,39 @@ func SetAdmin(name string, admin interop.Hash160) { putNameState(ctx, ns) } -// SetRecord adds new record of the specified type to the provided domain. -func SetRecord(name string, typ RecordType, data string) { +// SetRecord updates record of the specified type and ID. +func SetRecord(name string, typ RecordType, id byte, data string) { + ctx := storage.GetContext() + tokenID := checkRecord(ctx, name, typ, data) + recordKey := getRecordKey(tokenID, name, typ, id) + recBytes := storage.Get(ctx, recordKey) + if recBytes == nil { + panic("unknown record") + } + putRecord(ctx, tokenID, name, typ, id, data) +} + +// AddRecord adds new record of the specified type to the provided domain. +func AddRecord(name string, typ RecordType, data string) { + ctx := storage.GetContext() + tokenID := checkRecord(ctx, name, typ, data) + recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) + var id byte + records := storage.Find(ctx, recordsPrefix, storage.KeysOnly) + for iterator.Next(records) { + id++ + } + if id > maxRecordID { + panic("maximum number of records reached") + } + if typ == CNAME && id != 0 { + panic("multiple CNAME records") + } + putRecord(ctx, tokenID, name, typ, id, data) +} + +// checkRecord performs record validness check and returns token ID. +func checkRecord(ctx storage.Context, name string, typ RecordType, data string) []byte { tokenID := []byte(tokenIDFromName(name)) var ok bool switch typ { @@ -356,44 +391,46 @@ func SetRecord(name string, typ RecordType, data string) { if !ok { panic("invalid record data") } - ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - putRecord(ctx, tokenID, name, typ, data) + return tokenID } -// GetRecord returns domain record of the specified type if it exists or an empty -// string if not. -func GetRecord(name string, typ RecordType) string { +// GetRecords returns domain records of the specified type if they exist or an empty +// array if not. +func GetRecords(name string, typ RecordType) []string { tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() _ = getNameState(ctx, tokenID) // ensure not expired - return getRecord(ctx, tokenID, name, typ) + return getRecordsByType(ctx, tokenID, name, typ) } -// DeleteRecord removes domain record with the specified type. -func DeleteRecord(name string, typ RecordType) { +// DeleteRecords removes all domain records with the specified type. +func DeleteRecords(name string, typ RecordType) { tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - recordKey := getRecordKey(tokenID, name, typ) - storage.Delete(ctx, recordKey) + recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) + records := storage.Find(ctx, recordsPrefix, storage.KeysOnly) + for iterator.Next(records) { + key := iterator.Value(records).(string) + storage.Delete(ctx, key) + } } -// Resolve resolves given name (not more then three redirects are allowed). -func Resolve(name string, typ RecordType) string { +// Resolve resolves given name (not more than three redirects are allowed) to a set +// of domain records. +func Resolve(name string, typ RecordType) []string { ctx := storage.GetReadOnlyContext() - return resolve(ctx, name, typ, 2) + res := []string{} + return resolve(ctx, res, name, typ, 2) } // GetAllRecords returns an Iterator with RecordState items for given name. func GetAllRecords(name string) iterator.Iterator { - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() - _ = getNameState(ctx, tokenID) // ensure not expired - recordsKey := getRecordsKey(tokenID, name) - return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) + return getAllRecords(ctx, name) } // updateBalance updates account's balance and account's tokens. @@ -482,41 +519,53 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) { storage.Put(ctx, nameKey, nsBytes) } -// getRecord returns domain record. -func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string { - recordKey := getRecordKey(tokenId, name, typ) - recBytes := storage.Get(ctx, recordKey) - if recBytes == nil { - return recBytes.(string) // A hack to actually return NULL. +// getRecordsByType returns domain records of the specified type or an empty array if no records found. +func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string { + recordsPrefix := getRecordsByTypePrefix(tokenId, name, typ) + records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues) + res := []string{} // return empty slice if no records was found. + for iterator.Next(records) { + r := iterator.Value(records).(RecordState) + if r.Type == typ { + res = append(res, r.Data) + } } - record := std.Deserialize(recBytes.([]byte)).(RecordState) - return record.Data + return res } -// putRecord stores domain record. -func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) { - recordKey := getRecordKey(tokenId, name, typ) +// putRecord puts the specified record to the contract storage without any additional checks. +func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) { + recordKey := getRecordKey(tokenId, name, typ, id) rs := RecordState{ Name: name, Type: typ, - Data: record, + Data: data, + ID: id, } recBytes := std.Serialize(rs) storage.Put(ctx, recordKey, recBytes) } -// getRecordsKey returns prefix used to store domain records of different types. -func getRecordsKey(tokenId []byte, name string) []byte { - recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) - return append(recordKey, getTokenKey([]byte(name))...) +// getRecordKey returns key used to store domain record with the specified type and ID. +// This key always have a single corresponding value. +func getRecordKey(tokenId []byte, name string, typ RecordType, id byte) []byte { + prefix := getRecordsByTypePrefix(tokenId, name, typ) + return append(prefix, id) } -// getRecordKey returns key used to store domain records. -func getRecordKey(tokenId []byte, name string, typ RecordType) []byte { - recordKey := getRecordsKey(tokenId, name) +// getRecordsByTypePrefix returns prefix used to store domain records with the +// specified type of different IDs. +func getRecordsByTypePrefix(tokenId []byte, name string, typ RecordType) []byte { + recordKey := getRecordsPrefix(tokenId, name) return append(recordKey, []byte{byte(typ)}...) } +// getRecordsPrefix returns prefix used to store domain records of different types. +func getRecordsPrefix(tokenId []byte, name string) []byte { + recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) + return append(recordKey, getTokenKey([]byte(name))...) +} + // isValid returns true if the provided address is a valid Uint160. func isValid(address interop.Hash160) bool { return address != nil && len(address) == 20 @@ -713,7 +762,7 @@ func tokenIDFromName(name string) string { // resolve resolves provided name using record with the specified type and given // maximum redirections constraint. -func resolve(ctx storage.Context, name string, typ RecordType, redirect int) string { +func resolve(ctx storage.Context, res []string, name string, typ RecordType, redirect int) []string { if redirect < 0 { panic("invalid redirect") } @@ -723,33 +772,33 @@ func resolve(ctx storage.Context, name string, typ RecordType, redirect int) str if name[len(name)-1] == '.' { name = name[:len(name)-1] } - records := getRecords(ctx, name) + records := getAllRecords(ctx, name) cname := "" for iterator.Next(records) { - r := iterator.Value(records).(struct { - key string - rs RecordState - }) - value := r.rs.Data - rTyp := r.key[len(r.key)-1] - if rTyp == byte(typ) { - return value + r := iterator.Value(records).(RecordState) + if r.Type == typ { + res = append(res, r.Data) } - if rTyp == byte(CNAME) { - cname = value + if r.Type == CNAME { + cname = r.Data } } - if cname == "" { - return string([]byte(nil)) + if cname == "" || typ == CNAME { + return res } - return resolve(ctx, cname, typ, redirect-1) + + // TODO: the line below must be removed from the neofs nns: + // res = append(res, cname) + // @roman-khimov, it is done in a separate commit in neofs-contracts repo, is it OK? + return resolve(ctx, res, cname, typ, redirect-1) } -// getRecords returns iterator over the set of records corresponded with the -// specified name. -func getRecords(ctx storage.Context, name string) iterator.Iterator { +// getAllRecords returns iterator over the set of records corresponded with the +// specified name. Records returned are of different types and/or different IDs. +// No keys are returned. +func getAllRecords(ctx storage.Context, name string) iterator.Iterator { tokenID := []byte(tokenIDFromName(name)) - _ = getNameState(ctx, tokenID) - recordsKey := getRecordsKey(tokenID, name) - return storage.Find(ctx, recordsKey, storage.DeserializeValues) + _ = getNameState(ctx, tokenID) // ensure not expired. + recordsPrefix := getRecordsPrefix(tokenID, name) + return storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues) } diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 1f24f3bc9..4c2508172 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -2,7 +2,7 @@ name: "NameService" sourceurl: https://github.com/nspcc-dev/neo-go/ supportedstandards: ["NEP-11"] safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", - "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", + "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", "resolve", "getAllRecords"] events: - name: Transfer diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 39611b87e..664b08aaf 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -1,6 +1,8 @@ package nns_test import ( + "math/big" + "strconv" "strings" "testing" @@ -100,7 +102,7 @@ func TestExpiration(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) @@ -127,7 +129,7 @@ func TestExpiration(t *testing.T) { require.NoError(t, bc.AddBlock(e.SignBlock(b4))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) // TLD "com" has been expired - tx = cAcc.PrepareInvoke(t, "getRecord", "first.com", int64(nns.TXT)) + tx = cAcc.PrepareInvoke(t, "getRecords", "first.com", int64(nns.TXT)) b5 := e.NewUnsignedBlock(t, tx) b5.Index = b4.Index + 1 b5.PrevHash = b4.Hash() @@ -208,7 +210,7 @@ func TestRegisterAndRenew(t *testing.T) { c.Invoke(t, props, "properties", "neo.com") } -func TestSetGetRecord(t *testing.T) { +func TestSetAddGetRecord(t *testing.T) { c := newNSClient(t) e := c.Executor @@ -217,33 +219,56 @@ func TestSetGetRecord(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) t.Run("set before register", func(t *testing.T) { - c.InvokeFail(t, "token not found", "setRecord", "neo.com", int64(nns.TXT), "sometext") + c.InvokeFail(t, "token not found", "addRecord", "neo.com", int64(nns.TXT), "sometext") }) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) t.Run("invalid parameters", func(t *testing.T) { - c.InvokeFail(t, "unsupported record type", "setRecord", "neo.com", int64(0xFF), "1.2.3.4") - c.InvokeFail(t, "invalid record", "setRecord", "neo.com", int64(nns.A), "not.an.ip.address") + c.InvokeFail(t, "unsupported record type", "addRecord", "neo.com", int64(0xFF), "1.2.3.4") + c.InvokeFail(t, "invalid record", "addRecord", "neo.com", int64(nns.A), "not.an.ip.address") }) t.Run("invalid witness", func(t *testing.T) { - cAcc.InvokeFail(t, "not witnessed by admin", "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.InvokeFail(t, "not witnessed by admin", "addRecord", "neo.com", int64(nns.A), "1.2.3.4") }) - c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("1.2.3.4"), + stackitem.Make("1.2.3.4"), + }), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") + // Add multiple records and update some of them. + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext1") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext2") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext3") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("sometext"), + stackitem.Make("sometext1"), + stackitem.Make("sometext2"), + stackitem.Make("sometext3"), + }), "getRecords", "neo.com", int64(nns.TXT)) + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 2, "sometext22") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("sometext"), + stackitem.Make("sometext1"), + stackitem.Make("sometext22"), + stackitem.Make("sometext3"), + }), "getRecords", "neo.com", int64(nns.TXT)) // Delete record. t.Run("invalid witness", func(t *testing.T) { - cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME)) + cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.CNAME)) }) - c.Invoke(t, "nspcc.ru", "getRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("1.2.3.4"), + stackitem.Make("1.2.3.4"), + }), "getRecords", "neo.com", int64(nns.A)) t.Run("SetRecord_compatibility", func(t *testing.T) { // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior @@ -303,9 +328,9 @@ func TestSetGetRecord(t *testing.T) { args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name} t.Run(testCase.Name, func(t *testing.T) { if testCase.ShouldFail { - c.InvokeFail(t, "", "setRecord", args...) + c.InvokeFail(t, "", "addRecord", args...) } else { - c.Invoke(t, stackitem.Null{}, "setRecord", args...) + c.Invoke(t, stackitem.Null{}, "addRecord", args...) } }) } @@ -343,15 +368,15 @@ func TestSetAdmin(t *testing.T) { c.Invoke(t, props, "properties", "neo.com") t.Run("set and delete by admin", func(t *testing.T) { - cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") - cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) - cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") + cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.TXT)) }) t.Run("set admin to null", func(t *testing.T) { - cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) - cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT)) }) } @@ -367,7 +392,7 @@ func TestTransfer(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash()) - cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cFrom.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil) @@ -450,18 +475,27 @@ func TestResolve(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com") cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt from alias1") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.CNAME), "alias2.com") - c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) - c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) - c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT)) - c.Invoke(t, "sometxt", "resolve", "neo.com.", int64(nns.TXT)) - c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.TXT)) - c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) + cAccCommittee.Invoke(t, true, "register", "alias2.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias2.com", int64(nns.TXT), "sometxt from alias2") + + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com.", int64(nns.A)) + c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.A)) + + // Check CNAME is properly resolved and is not included into the result. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("sometxt from alias1"), stackitem.Make("sometxt from alias2")}), "resolve", "neo.com", int64(nns.TXT)) + // Check CNAME is included into the result and is not resolved. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("alias.com")}), "resolve", "neo.com", int64(nns.CNAME)) + + // Empty result. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "neo.com", int64(nns.AAAA)) } func TestGetAllRecords(t *testing.T) { @@ -474,14 +508,14 @@ func TestGetAllRecords(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla0") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla1") // overwrite + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "bla0") + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 0, "bla1") // overwrite // Add some arbitrary data. cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt") script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com") require.NoError(t, err) @@ -491,21 +525,63 @@ func TestGetAllRecords(t *testing.T) { stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.A), stackitem.NewByteArray([]byte("1.2.3.4")), + stackitem.NewBigInteger(big.NewInt(0)), }), stackitem.NewStruct([]stackitem.Item{ stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.CNAME), stackitem.NewByteArray([]byte("alias.com")), + stackitem.NewBigInteger(big.NewInt(0)), }), stackitem.NewStruct([]stackitem.Item{ stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.TXT), stackitem.NewByteArray([]byte("bla1")), + stackitem.NewBigInteger(big.NewInt(0)), }), })) } +func TestGetRecords(t *testing.T) { + c := newNSClient(t) + e := c.Executor + + acc := e.NewAccount(t) + cAcc := c.WithSigners(acc) + cAccCommittee := c.WithSigners(acc, c.Committee) + + c.Invoke(t, true, "register", "com", c.CommitteeHash) + cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com") + + // Add some arbitrary data. + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt") + + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) + // Check empty result of `getRecords`. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.AAAA)) +} + +func TestNNSAddRecord(t *testing.T) { + c := newNSClient(t) + cAccCommittee := c.WithSigners(c.Committee) + + c.Invoke(t, true, "register", "com", c.CommitteeHash) + cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash) + + for i := 0; i <= maxRecordID+1; i++ { + if i == maxRecordID+1 { + c.InvokeFail(t, "maximum number of records reached", "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i)) + } else { + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i)) + } + } +} + const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 + maxRecordID = 255 ) diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index f6683bf2f..7dff03d7b 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -177,7 +177,7 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) { t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID)) // Block #15: set A record type with priv0 owner via NNS. - nsPriv0Invoker.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 + nsPriv0Invoker.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1