From 16aaaa637c2fdb15daab935d5d6ea074596a8b70 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:38:38 +0300 Subject: [PATCH 01/17] [#266] nns: Add `admin` to properties Follow the https://github.com/neo-project/non-native-contracts/blob/14f43ba8cf169323b61c23a3a701ac77d9a4e3eb/src/NameService/NameService.cs#L69. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 1 + tests/nns_test.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 8894294b..2ad91157 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -135,6 +135,7 @@ func Properties(tokenID []byte) map[string]interface{} { return map[string]interface{}{ "name": ns.Name, "expiration": ns.Expiration, + "admin": ns.Admin, } } diff --git a/tests/nns_test.go b/tests/nns_test.go index 2f8049ca..0e312437 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -262,7 +262,8 @@ func TestExpiration(t *testing.T) { checkProperties := func(t *testing.T, expiration uint64) { expected := stackitem.NewMapWithValue([]stackitem.MapElement{ {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")}, - {Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)}}) + {Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)}, + {Key: stackitem.Make("admin"), Value: stackitem.Null{}}}) s, err := c.TestInvoke(t, "properties", "testdomain.com") require.NoError(t, err) require.Equal(t, expected.Value(), s.Top().Item().Value()) @@ -296,6 +297,7 @@ func TestNNSSetAdmin(t *testing.T) { c.Invoke(t, true, "register", "testdomain.com", c.CommitteeHash, "myemail@nspcc.ru", refresh, retry, expire, ttl) + top := c.TopBlock(t) acc := c.NewAccount(t) cAcc := c.WithSigners(acc) @@ -305,6 +307,13 @@ func TestNNSSetAdmin(t *testing.T) { c1 := c.WithSigners(c.Committee, acc) c1.Invoke(t, stackitem.Null{}, "setAdmin", "testdomain.com", acc.ScriptHash()) + expiration := top.Timestamp + uint64(expire*1000) + expectedProps := stackitem.NewMapWithValue([]stackitem.MapElement{ + {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")}, + {Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)}, + {Key: stackitem.Make("admin"), Value: stackitem.Make(acc.ScriptHash().BytesBE())}}) + cAcc.Invoke(t, expectedProps, "properties", "testdomain.com") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), "will be added") } @@ -351,7 +360,8 @@ func TestNNSRenew(t *testing.T) { c1.Invoke(t, ts, "renew", "testdomain.com") expected := stackitem.NewMapWithValue([]stackitem.MapElement{ {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")}, - {Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)}}) + {Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)}, + {Key: stackitem.Make("admin"), Value: stackitem.Null{}}}) cAcc.Invoke(t, expected, "properties", "testdomain.com") } From 80e5bcb2cecde97922e48e3cab3e7c5e6e9007a6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:39:34 +0300 Subject: [PATCH 02/17] [#266] nns: Return empty Array from `resolve` instead of Null In case if no records of the specified type found. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 3 ++- tests/container_test.go | 2 +- tests/nns_test.go | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 2ad91157..ed2ea2a4 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -443,7 +443,8 @@ func DeleteRecords(name string, typ RecordType) { // Resolve resolves given name (not more then three redirects are allowed). func Resolve(name string, typ RecordType) []string { ctx := storage.GetReadOnlyContext() - return resolve(ctx, nil, name, typ, 2) + res := []string{} + return resolve(ctx, res, name, typ, 2) } // GetAllRecords returns an Iterator with RecordState items for the given name. diff --git a/tests/container_test.go b/tests/container_test.go index 9a265e17..d26221c5 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -153,7 +153,7 @@ func TestContainerPut(t *testing.T) { }) c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.token) - cNNS.Invoke(t, stackitem.Null{}, "resolve", "mycnt.neofs", int64(nns.TXT)) + cNNS.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "mycnt.neofs", int64(nns.TXT)) t.Run("register in advance", func(t *testing.T) { cnt.value[len(cnt.value)-1] = 10 diff --git a/tests/nns_test.go b/tests/nns_test.go index 0e312437..91fc7ec5 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -380,4 +380,6 @@ func TestNNSResolve(t *testing.T) { c.Invoke(t, records, "resolve", "test.com", int64(nns.TXT)) c.Invoke(t, records, "resolve", "test.com.", int64(nns.TXT)) c.InvokeFail(t, "invalid domain name format", "resolve", "test.com..", int64(nns.TXT)) + // Empty result. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "test.com", int64(nns.CNAME)) } From 53e0e91531d6a8ea4f479ae1a4941e31d1ba999c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:40:28 +0300 Subject: [PATCH 03/17] [#266] nns: Fix safe methods list `getRecord` doesn't exist since https://github.com/nspcc-dev/neofs-contract/pull/133/commits/6ea4573ef86c445709c792f4b40c7ae200e7d799. Signed-off-by: Anna Shaleva --- nns/nns.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nns/nns.yml b/nns/nns.yml index 289793c9..155fa9da 100644 --- a/nns/nns.yml +++ b/nns/nns.yml @@ -1,7 +1,7 @@ name: "NameService" 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 From b584d5afa1d1b0ada97762f835e1a4ec76bfae00 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:40:35 +0300 Subject: [PATCH 04/17] [#266] nns: Return empty Array from getRecords instead of Null And adjust method usages along the way. Signed-off-by: Anna Shaleva --- container/container_contract.go | 8 ++++---- nns/nns_contract.go | 4 ++-- tests/nns_test.go | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/container/container_contract.go b/container/container_contract.go index c3721238..abf210b0 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -295,8 +295,8 @@ func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool } res := contract.Call(nnsContractAddr, "getRecords", - contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */) - if res != nil { + contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */).([]string) + if len(res) > 0 { panic("name is already taken") } @@ -351,8 +351,8 @@ func Delete(containerID []byte, signature interop.Signature, token []byte) { // by other means (expiration, manual), thus leading to failing `deleteRecord` // and inability to delete a container. We should also check if we own the record in case. nnsContractAddr := storage.Get(ctx, nnsContractKey).(interop.Hash160) - res := contract.Call(nnsContractAddr, "getRecords", contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */) - if res != nil && std.Base58Encode(containerID) == string(res.([]interface{})[0].(string)) { + res := contract.Call(nnsContractAddr, "getRecords", contract.ReadStates|contract.AllowCall, domain, 16 /* TXT */).([]string) + if len(res) > 0 && std.Base58Encode(containerID) == res[0] { contract.Call(nnsContractAddr, "deleteRecords", contract.All, domain, 16 /* TXT */) } } diff --git a/nns/nns_contract.go b/nns/nns_contract.go index ed2ea2a4..add8f6b8 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -542,11 +542,11 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) { storage.Put(ctx, nameKey, nsBytes) } -// getRecordsByType returns domain record. +// getRecordsByType returns domain record. It returns empty array if no records found. func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string { recordsKey := getRecordsKeyByType(tokenId, name, typ) - var result []string + result := []string{} records := storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) for iterator.Next(records) { r := iterator.Value(records).(RecordState) diff --git a/tests/nns_test.go b/tests/nns_test.go index 91fc7ec5..6b1b69d1 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -251,6 +251,23 @@ func TestNNSGetAllRecords(t *testing.T) { require.False(t, iter.Next()) } +func TestNNSGetRecords(t *testing.T) { + c := newNNSInvoker(t, true) + + refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) + c.Invoke(t, true, "register", + "testdomain.com", c.CommitteeHash, + "myemail@nspcc.ru", refresh, retry, expire, ttl) + + txtData := "first TXT record" + c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), txtData) + c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.A), "1.2.3.4") + + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make(txtData)}), "getRecords", "testdomain.com", int64(nns.TXT)) + // Check empty result. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "testdomain.com", int64(nns.AAAA)) +} + func TestExpiration(t *testing.T) { c := newNNSInvoker(t, true) From 8213ba52929f4fb60291ff5c1286a7388e450968 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:40:48 +0300 Subject: [PATCH 05/17] [#266] nns: Fix typo in the method description Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index add8f6b8..e34148f8 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -440,7 +440,7 @@ func DeleteRecords(name string, typ RecordType) { updateSoaSerial(ctx, tokenID) } -// Resolve resolves given name (not more then three redirects are allowed). +// Resolve resolves given name (not more than three redirects are allowed). func Resolve(name string, typ RecordType) []string { ctx := storage.GetReadOnlyContext() res := []string{} From b1f5864942138d8667badd0f7c583417a5bc593c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:40:54 +0300 Subject: [PATCH 06/17] [#266] nns: Restrict the maximum number of records with the same type Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 6 ++++++ tests/nns_test.go | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index e34148f8..4f7f4faa 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -57,6 +57,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. @@ -582,6 +585,9 @@ func addRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, panic("record already exists") } } + if id > maxRecordID { + panic("maximum number of records reached") + } if typ == CNAME && id != 0 { panic("you shouldn't have more than one CNAME record") diff --git a/tests/nns_test.go b/tests/nns_test.go index 6b1b69d1..381217c7 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" "path" + "strconv" "strings" "testing" "time" @@ -17,7 +18,10 @@ import ( const nnsPath = "../nns" -const msPerYear = 365 * 24 * time.Hour / time.Millisecond +const ( + msPerYear = 365 * 24 * time.Hour / time.Millisecond + maxRecordID = 255 // value from the contract. +) func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker { e := newExecutor(t) @@ -400,3 +404,19 @@ func TestNNSResolve(t *testing.T) { // Empty result. c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "test.com", int64(nns.CNAME)) } + +func TestNNSAddRecord(t *testing.T) { + c := newNNSInvoker(t, true) + + refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) + c.Invoke(t, true, "register", + "testdomain.com", c.CommitteeHash, + "myemail@nspcc.ru", refresh, retry, expire, ttl) + for i := 0; i <= maxRecordID+1; i++ { + if i == maxRecordID+1 { + c.InvokeFail(t, "maximum number of records reached", "addRecord", "testdomain.com", int64(nns.TXT), strconv.Itoa(i)) + } else { + c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), strconv.Itoa(i)) + } + } +} From 48c96ec5838930f1ff37e34f8cc1fe805544b7e0 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:41:02 +0300 Subject: [PATCH 07/17] [#266] nns: Move common code to a separate method Reuse getAllRecords for GetAllRecords. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 4f7f4faa..ddf7358a 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -452,11 +452,8 @@ func Resolve(name string, typ RecordType) []string { // GetAllRecords returns an Iterator with RecordState items for the 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. @@ -913,7 +910,7 @@ func resolve(ctx storage.Context, res []string, name string, typ RecordType, red // specified name. func getAllRecords(ctx storage.Context, name string) iterator.Iterator { tokenID := []byte(tokenIDFromName(name)) - _ = getNameState(ctx, tokenID) + _ = getNameState(ctx, tokenID) // ensure not expired. recordsKey := getRecordsKey(tokenID, name) return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) } From f65bbc00a3150d2f60a0123984da792ac33d4208 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:41:21 +0300 Subject: [PATCH 08/17] [#266] nns: Fix CNAME resolution rules Do not include CNAME to the resulting list if we're looking for another record type. If it's CNAME than it must be resolved. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 1 - tests/nns_test.go | 22 ++++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index ddf7358a..f593f2eb 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -902,7 +902,6 @@ func resolve(ctx storage.Context, res []string, name string, typ RecordType, red return res } - res = append(res, cname) return resolve(ctx, res, cname, typ, redirect-1) } diff --git a/tests/nns_test.go b/tests/nns_test.go index 381217c7..2947b61d 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -390,19 +390,29 @@ func TestNNSResolve(t *testing.T) { c := newNNSInvoker(t, true) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) - c.Invoke(t, true, "register", - "test.com", c.CommitteeHash, - "myemail@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, true, "register", "test.com", c.CommitteeHash, "myemail@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, stackitem.Null{}, "addRecord", "test.com", int64(nns.TXT), "expected result") + c.Invoke(t, stackitem.Null{}, "addRecord", "test.com", int64(nns.CNAME), "alias.com") - c.Invoke(t, stackitem.Null{}, "addRecord", - "test.com", int64(nns.TXT), "expected result") + c.Invoke(t, true, "register", "alias.com", c.CommitteeHash, "myemail@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.CNAME), "alias2.com") + + c.Invoke(t, true, "register", "alias2.com", c.CommitteeHash, "myemail@nspcc.ru", refresh, retry, expire, ttl) + c.Invoke(t, stackitem.Null{}, "addRecord", "alias2.com", int64(nns.A), "5.6.7.8") records := stackitem.NewArray([]stackitem.Item{stackitem.Make("expected result")}) c.Invoke(t, records, "resolve", "test.com", int64(nns.TXT)) c.Invoke(t, records, "resolve", "test.com.", int64(nns.TXT)) c.InvokeFail(t, "invalid domain name format", "resolve", "test.com..", int64(nns.TXT)) + // Empty result. - c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "test.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "test.com", int64(nns.AAAA)) + + // Check CNAME is properly resolved and is not included into the result list. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4"), stackitem.Make("5.6.7.8")}), "resolve", "test.com", int64(nns.A)) + // And this time it should be properly included without resolution. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("alias.com")}), "resolve", "test.com", int64(nns.CNAME)) } func TestNNSAddRecord(t *testing.T) { From fe266fd727d805e7ce38cffc0acb97d874ea45d2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 12 Sep 2022 10:30:21 +0300 Subject: [PATCH 09/17] [#266] nns: Remove unused config file Signed-off-by: Anna Shaleva --- nns/nns.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 nns/nns.yml diff --git a/nns/nns.yml b/nns/nns.yml deleted file mode 100644 index 155fa9da..00000000 --- a/nns/nns.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: "NameService" -supportedstandards: ["NEP-11"] -safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", - "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", - "resolve", "getAllRecords"] -events: - - name: Transfer - parameters: - - name: from - type: Hash160 - - name: to - type: Hash160 - - name: amount - type: Integer - - name: tokenId - type: ByteArray -permissions: - - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd - methods: ["update"] - - methods: ["onNEP11Payment"] From 22f2700dfd2e62d2cc6ffa42b56ede54023e928d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 13 Sep 2022 12:37:26 +0300 Subject: [PATCH 10/17] [#266] nns: Refactor record-related operations code Do not move parts of SetRecord/AddRecord to a separate functions, it makes the contract code more complicated. Also, improve documentation a bit. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 73 +++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index f593f2eb..52c4d5e4 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -375,7 +375,7 @@ func SetAdmin(name string, admin interop.Hash160) { putNameState(ctx, ns) } -// SetRecord adds a new record of the specified type to the provided domain. +// SetRecord updates existing domain record with the specified type and ID. func SetRecord(name string, typ RecordType, id byte, data string) { tokenID := []byte(tokenIDFromName(name)) if !checkBaseRecords(typ, data) { @@ -384,7 +384,12 @@ func SetRecord(name string, typ RecordType, id byte, data string) { ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - putRecord(ctx, tokenID, name, typ, id, data) + recordKey := getIdRecordKey(tokenID, name, typ, id) + recBytes := storage.Get(ctx, recordKey) + if recBytes == nil { + panic("invalid record id") + } + storeRecord(ctx, recordKey, name, typ, id, data) updateSoaSerial(ctx, tokenID) } @@ -403,7 +408,8 @@ func checkBaseRecords(typ RecordType, data string) bool { } } -// AddRecord adds a new record of the specified type to the provided domain. +// AddRecord appends domain record to the list of domain records with the specified type +// if it doesn't exist yet. func AddRecord(name string, typ RecordType, data string) { tokenID := []byte(tokenIDFromName(name)) if !checkBaseRecords(typ, data) { @@ -412,7 +418,27 @@ func AddRecord(name string, typ RecordType, data string) { ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - addRecord(ctx, tokenID, name, typ, data) + recordsKey := getRecordsKeyByType(tokenID, name, typ) + var id byte + records := storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) + for iterator.Next(records) { + id++ + + r := iterator.Value(records).(RecordState) + if r.Name == name && r.Type == typ && r.Data == data { + panic("record already exists") + } + } + if id > maxRecordID { + panic("maximum number of records reached") + } + + if typ == CNAME && id != 0 { + panic("you shouldn't have more than one CNAME record") + } + + recordKey := append(recordsKey, id) // the same as getIdRecordKey + storeRecord(ctx, recordKey, name, typ, id, data) updateSoaSerial(ctx, tokenID) } @@ -557,44 +583,7 @@ func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ Reco return result } -// putRecord stores domain record. -func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) { - recordKey := getIdRecordKey(tokenId, name, typ, id) - recBytes := storage.Get(ctx, recordKey) - if recBytes == nil { - panic("invalid record id") - } - - storeRecord(ctx, recordKey, name, typ, id, data) -} - -// addRecord stores domain record. -func addRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, data string) { - recordsKey := getRecordsKeyByType(tokenId, name, typ) - - var id byte - records := storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) - for iterator.Next(records) { - id++ - - r := iterator.Value(records).(RecordState) - if r.Name == name && r.Type == typ && r.Data == data { - panic("record already exists") - } - } - if id > maxRecordID { - panic("maximum number of records reached") - } - - if typ == CNAME && id != 0 { - panic("you shouldn't have more than one CNAME record") - } - - recordKey := append(recordsKey, id) // the same as getIdRecordKey - storeRecord(ctx, recordKey, name, typ, id, data) -} - -// storeRecord puts record to storage. +// storeRecord puts record to storage and performs no additional checks. func storeRecord(ctx storage.Context, recordKey []byte, name string, typ RecordType, id byte, data string) { rs := RecordState{ Name: name, From e5ad839ba0f744268e6a337b81db1bb28f1bf7d3 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 13 Sep 2022 12:47:29 +0300 Subject: [PATCH 11/17] [#266] nns: Move common record checking code to a separate function Don't repeat it each time. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 52c4d5e4..c7e6209e 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -377,48 +377,47 @@ func SetAdmin(name string, admin interop.Hash160) { // SetRecord updates existing domain record with the specified type and ID. func SetRecord(name string, typ RecordType, id byte, data string) { - tokenID := []byte(tokenIDFromName(name)) - if !checkBaseRecords(typ, data) { - panic("invalid record data") - } ctx := storage.GetContext() - ns := getNameState(ctx, tokenID) - ns.checkAdmin() - recordKey := getIdRecordKey(tokenID, name, typ, id) + tokenId := checkRecord(ctx, name, typ, data) + recordKey := getIdRecordKey(tokenId, name, typ, id) recBytes := storage.Get(ctx, recordKey) if recBytes == nil { panic("invalid record id") } storeRecord(ctx, recordKey, name, typ, id, data) - updateSoaSerial(ctx, tokenID) + updateSoaSerial(ctx, tokenId) } -func checkBaseRecords(typ RecordType, data string) bool { +// 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 { case A: - return checkIPv4(data) + ok = checkIPv4(data) case CNAME: - return splitAndCheck(data, true) != nil + ok = splitAndCheck(data, true) != nil case TXT: - return len(data) <= maxTXTRecordLength + ok = len(data) <= maxTXTRecordLength case AAAA: - return checkIPv6(data) + ok = checkIPv6(data) default: panic("unsupported record type") } + if !ok { + panic("invalid record data") + } + ns := getNameState(ctx, tokenID) + ns.checkAdmin() + return tokenID } // AddRecord appends domain record to the list of domain records with the specified type // if it doesn't exist yet. func AddRecord(name string, typ RecordType, data string) { - tokenID := []byte(tokenIDFromName(name)) - if !checkBaseRecords(typ, data) { - panic("invalid record data") - } ctx := storage.GetContext() - ns := getNameState(ctx, tokenID) - ns.checkAdmin() - recordsKey := getRecordsKeyByType(tokenID, name, typ) + tokenId := checkRecord(ctx, name, typ, data) + recordsKey := getRecordsKeyByType(tokenId, name, typ) var id byte records := storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) for iterator.Next(records) { @@ -439,7 +438,7 @@ func AddRecord(name string, typ RecordType, data string) { recordKey := append(recordsKey, id) // the same as getIdRecordKey storeRecord(ctx, recordKey, name, typ, id, data) - updateSoaSerial(ctx, tokenID) + updateSoaSerial(ctx, tokenId) } // GetRecords returns domain record of the specified type if it exists or an empty From 86d171e7a1c6d5b1d60fa9daf388ef3e1419ed87 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 13 Sep 2022 12:50:04 +0300 Subject: [PATCH 12/17] [#266] nns: Accept token ID as an argument for storeRecord It doesn't save VM opcodes, but allows to keep record key creation logic in a single place which prevents the code from bugs appearance. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index c7e6209e..e58ea0c6 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -384,7 +384,7 @@ func SetRecord(name string, typ RecordType, id byte, data string) { if recBytes == nil { panic("invalid record id") } - storeRecord(ctx, recordKey, name, typ, id, data) + storeRecord(ctx, tokenId, name, typ, id, data) updateSoaSerial(ctx, tokenId) } @@ -436,8 +436,7 @@ func AddRecord(name string, typ RecordType, data string) { panic("you shouldn't have more than one CNAME record") } - recordKey := append(recordsKey, id) // the same as getIdRecordKey - storeRecord(ctx, recordKey, name, typ, id, data) + storeRecord(ctx, tokenId, name, typ, id, data) updateSoaSerial(ctx, tokenId) } @@ -583,7 +582,8 @@ func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ Reco } // storeRecord puts record to storage and performs no additional checks. -func storeRecord(ctx storage.Context, recordKey []byte, name string, typ RecordType, id byte, data string) { +func storeRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) { + recordKey := getIdRecordKey(tokenId, name, typ, id) rs := RecordState{ Name: name, Type: typ, From b5d1b61bb176e519eca86d1d83f1f71a84c9603a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 13 Sep 2022 12:56:40 +0300 Subject: [PATCH 13/17] [#266] nns: Reuse storeRecord for storing SOA record Less code repeating. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index e58ea0c6..424543b7 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -596,22 +596,14 @@ func storeRecord(ctx storage.Context, tokenId []byte, name string, typ RecordTyp // putSoaRecord stores soa domain record. func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expire, ttl int) { - var id byte tokenId := []byte(tokenIDFromName(name)) - recordKey := getIdRecordKey(tokenId, name, SOA, id) - rs := RecordState{ - Name: name, - Type: SOA, - ID: id, - Data: name + " " + email + " " + - std.Itoa(runtime.GetTime(), 10) + " " + - std.Itoa(refresh, 10) + " " + - std.Itoa(retry, 10) + " " + - std.Itoa(expire, 10) + " " + - std.Itoa(ttl, 10), - } - recBytes := std.Serialize(rs) - storage.Put(ctx, recordKey, recBytes) + data := name + " " + email + " " + + std.Itoa(runtime.GetTime(), 10) + " " + + std.Itoa(refresh, 10) + " " + + std.Itoa(retry, 10) + " " + + std.Itoa(expire, 10) + " " + + std.Itoa(ttl, 10) + storeRecord(ctx, tokenId, name, SOA, 0, data) } // updateSoaSerial stores soa domain record. From 2d8ba9d72e303f4f71ebbcc32a3c41f918cb2fba Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 13 Sep 2022 17:24:16 +0300 Subject: [PATCH 14/17] [#266] nns: Move token key creation to a separate function Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 424543b7..da6fad5d 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -543,7 +543,7 @@ func getNameState(ctx storage.Context, tokenID []byte) NameState { // getNameStateWithKey returns domain name state by the specified token key. func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { - nameKey := append([]byte{prefixName}, tokenKey...) + nameKey := getNameStateKey(tokenKey) nsBytes := storage.Get(ctx, nameKey) if nsBytes == nil { panic("token not found") @@ -553,6 +553,11 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { return ns } +// getNameStateKey returns NameState key for the provided token key. +func getNameStateKey(tokenKey []byte) []byte { + return append([]byte{prefixName}, tokenKey...) +} + // putNameState stores domain name state. func putNameState(ctx storage.Context, ns NameState) { tokenKey := getTokenKey([]byte(ns.Name)) @@ -842,7 +847,7 @@ func tokenIDFromName(name string) string { l := len(fragments) - 1 for i := 0; i < l; i++ { tokenKey := getTokenKey([]byte(name[sum:])) - nameKey := append([]byte{prefixName}, tokenKey...) + nameKey := getNameStateKey(tokenKey) nsBytes := storage.Get(ctx, nameKey) if nsBytes != nil { ns := std.Deserialize(nsBytes.([]byte)).(NameState) From c2e411c691e28bed15f623b777dcc91f22fc3024 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 13 Sep 2022 17:26:45 +0300 Subject: [PATCH 15/17] [#266] nns: reuse existing context in tokenIDFromName Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index da6fad5d..2979b621 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -390,7 +390,7 @@ func SetRecord(name string, typ RecordType, id byte, data string) { // 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)) + tokenID := []byte(tokenIDFromName(ctx, name)) var ok bool switch typ { case A: @@ -443,8 +443,8 @@ func AddRecord(name string, typ RecordType, data string) { // GetRecords returns domain record of the specified type if it exists or an empty // string if not. func GetRecords(name string, typ RecordType) []string { - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() + tokenID := []byte(tokenIDFromName(ctx, name)) _ = getNameState(ctx, tokenID) // ensure not expired return getRecordsByType(ctx, tokenID, name, typ) } @@ -454,8 +454,8 @@ func DeleteRecords(name string, typ RecordType) { if typ == SOA { panic("you cannot delete soa record") } - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext() + tokenID := []byte(tokenIDFromName(ctx, name)) ns := getNameState(ctx, tokenID) ns.checkAdmin() recordsKey := getRecordsKeyByType(tokenID, name, typ) @@ -601,7 +601,7 @@ func storeRecord(ctx storage.Context, tokenId []byte, name string, typ RecordTyp // putSoaRecord stores soa domain record. func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expire, ttl int) { - tokenId := []byte(tokenIDFromName(name)) + tokenId := []byte(tokenIDFromName(ctx, name)) data := name + " " + email + " " + std.Itoa(runtime.GetTime(), 10) + " " + std.Itoa(refresh, 10) + " " + @@ -836,13 +836,12 @@ func checkIPv6(data string) bool { } // tokenIDFromName returns token ID (domain.root) from the provided name. -func tokenIDFromName(name string) string { +func tokenIDFromName(ctx storage.Context, name string) string { fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } - ctx := storage.GetReadOnlyContext() sum := 0 l := len(fragments) - 1 for i := 0; i < l; i++ { @@ -893,7 +892,7 @@ func resolve(ctx storage.Context, res []string, name string, typ RecordType, red // getAllRecords returns iterator over the set of records corresponded with the // specified name. func getAllRecords(ctx storage.Context, name string) iterator.Iterator { - tokenID := []byte(tokenIDFromName(name)) + tokenID := []byte(tokenIDFromName(ctx, name)) _ = getNameState(ctx, tokenID) // ensure not expired. recordsKey := getRecordsKey(tokenID, name) return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) From 6f49a54de0ab95045085df68a2260b21fb5b6964 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 14 Sep 2022 12:59:39 +0300 Subject: [PATCH 16/17] [#266] nns: Keep `isAvailable` in sync with `register` If conflicting records '*.domain' are present on new domain registration, then `isAvailable` should return false for this domain. Ref. https://github.com/nspcc-dev/neofs-contract/pull/175/commits/f25296b17a4dcaca50855c40d44a42bbcf0bb6a1. Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 35 ++++++++++++++++++++++++----------- tests/nns_test.go | 2 ++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 2979b621..2f642834 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -224,7 +224,7 @@ func GetPrice() int { // IsAvailable checks whether the provided domain name is available. func IsAvailable(name string) bool { - fragments := splitAndCheck(name, false) + fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } @@ -236,7 +236,27 @@ func IsAvailable(name string) bool { } return true } - return parentExpired(ctx, 0, fragments) + if !parentExpired(ctx, 0, fragments) { + return false + } + return len(getParentConflictingRecord(ctx, name, fragments)) == 0 +} + +// getPrentConflictingRecord returns record of '*.name' format if they are presented. +// These records conflict with domain name to be registered. +func getParentConflictingRecord(ctx storage.Context, name string, fragments []string) string { + parentKey := getTokenKey([]byte(name[len(fragments[0])+1:])) + 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) { + return r.Name + } + } + return "" } // parentExpired returns true if any domain from fragments doesn't exist or is expired. @@ -290,15 +310,8 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, 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) - } + if conflict := getParentConflictingRecord(ctx, name, fragments); len(conflict) != 0 { + panic("parent domain has conflicting records: " + conflict) } } diff --git a/tests/nns_test.go b/tests/nns_test.go index 2947b61d..bac6c1d3 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -178,11 +178,13 @@ func TestNNSRegisterMulti(t *testing.T) { "another.fs.neo.com", int64(nns.A), "4.3.2.1") c2 = c.WithSigners(acc, acc2) + c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.fs.neo.com") c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com", "register", args...) c1.Invoke(t, stackitem.Null{}, "deleteRecords", "something.mainnet.fs.neo.com", int64(nns.A)) + c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.fs.neo.com") c2.Invoke(t, true, "register", args...) c2 = c.WithSigners(acc2) From 37d5f5632efdae2111df795460943758683a50ff Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 14 Sep 2022 17:41:19 +0300 Subject: [PATCH 17/17] [#266] nns: Use millisecondsInSeconds constant where appropriate Signed-off-by: Anna Shaleva --- nns/nns_contract.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 2f642834..fae15a18 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -66,8 +66,10 @@ const ( const ( // defaultRegisterPrice is the default price for new domain registration. defaultRegisterPrice = 10_0000_0000 + // millisecondsInSecond is the amount of milliseconds per second. + millisecondsInSecond = 1000 // millisecondsInYear is amount of milliseconds per year. - millisecondsInYear = int64(365 * 24 * 3600 * 1000) + millisecondsInYear = int64(365 * 24 * 3600 * millisecondsInSecond) ) // RecordState is a type that registered entities are saved to. @@ -339,7 +341,7 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, Owner: owner, Name: name, // NNS expiration is in milliseconds - Expiration: int64(runtime.GetTime() + expire*1000), + Expiration: int64(runtime.GetTime() + expire*millisecondsInSecond), } putNameStateWithKey(ctx, tokenKey, ns) putSoaRecord(ctx, name, email, refresh, retry, expire, ttl)