nns: allow multiple records of the same type

Except for the CNAME records. Port
6ea4573ef8
and
f4762c1b56.
This commit is contained in:
Anna Shaleva 2022-09-09 19:36:13 +03:00
parent c296f8804c
commit c9050cef4b
4 changed files with 228 additions and 103 deletions

View file

@ -55,6 +55,9 @@ const (
maxDomainNameLength = 255 maxDomainNameLength = 255
// maxTXTRecordLength is the maximum length of the TXT domain record. // maxTXTRecordLength is the maximum length of the TXT domain record.
maxTXTRecordLength = 255 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. // Other constants.
@ -70,6 +73,7 @@ type RecordState struct {
Name string Name string
Type RecordType Type RecordType
Data string Data string
ID byte
} }
// Update updates NameService contract. // Update updates NameService contract.
@ -337,8 +341,39 @@ func SetAdmin(name string, admin interop.Hash160) {
putNameState(ctx, ns) putNameState(ctx, ns)
} }
// SetRecord adds new record of the specified type to the provided domain. // SetRecord updates record of the specified type and ID.
func SetRecord(name string, typ RecordType, data string) { 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)) tokenID := []byte(tokenIDFromName(name))
var ok bool var ok bool
switch typ { switch typ {
@ -356,44 +391,46 @@ func SetRecord(name string, typ RecordType, data string) {
if !ok { if !ok {
panic("invalid record data") panic("invalid record data")
} }
ctx := storage.GetContext()
ns := getNameState(ctx, tokenID) ns := getNameState(ctx, tokenID)
ns.checkAdmin() ns.checkAdmin()
putRecord(ctx, tokenID, name, typ, data) return tokenID
} }
// GetRecord returns domain record of the specified type if it exists or an empty // GetRecords returns domain records of the specified type if they exist or an empty
// string if not. // array if not.
func GetRecord(name string, typ RecordType) string { func GetRecords(name string, typ RecordType) []string {
tokenID := []byte(tokenIDFromName(name)) tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired _ = 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. // DeleteRecords removes all domain records with the specified type.
func DeleteRecord(name string, typ RecordType) { func DeleteRecords(name string, typ RecordType) {
tokenID := []byte(tokenIDFromName(name)) tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetContext() ctx := storage.GetContext()
ns := getNameState(ctx, tokenID) ns := getNameState(ctx, tokenID)
ns.checkAdmin() ns.checkAdmin()
recordKey := getRecordKey(tokenID, name, typ) recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ)
storage.Delete(ctx, recordKey) 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). // Resolve resolves given name (not more than three redirects are allowed) to a set
func Resolve(name string, typ RecordType) string { // of domain records.
func Resolve(name string, typ RecordType) []string {
ctx := storage.GetReadOnlyContext() 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. // GetAllRecords returns an Iterator with RecordState items for given name.
func GetAllRecords(name string) iterator.Iterator { func GetAllRecords(name string) iterator.Iterator {
tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired return getAllRecords(ctx, name)
recordsKey := getRecordsKey(tokenID, name)
return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues)
} }
// updateBalance updates account's balance and account's tokens. // 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) storage.Put(ctx, nameKey, nsBytes)
} }
// getRecord returns domain record. // getRecordsByType returns domain records of the specified type or an empty array if no records found.
func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string { func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string {
recordKey := getRecordKey(tokenId, name, typ) recordsPrefix := getRecordsByTypePrefix(tokenId, name, typ)
recBytes := storage.Get(ctx, recordKey) records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
if recBytes == nil { res := []string{} // return empty slice if no records was found.
return recBytes.(string) // A hack to actually return NULL. 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 res
return record.Data
} }
// putRecord stores domain record. // putRecord puts the specified record to the contract storage without any additional checks.
func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) { func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) {
recordKey := getRecordKey(tokenId, name, typ) recordKey := getRecordKey(tokenId, name, typ, id)
rs := RecordState{ rs := RecordState{
Name: name, Name: name,
Type: typ, Type: typ,
Data: record, Data: data,
ID: id,
} }
recBytes := std.Serialize(rs) recBytes := std.Serialize(rs)
storage.Put(ctx, recordKey, recBytes) storage.Put(ctx, recordKey, recBytes)
} }
// getRecordsKey returns prefix used to store domain records of different types. // getRecordKey returns key used to store domain record with the specified type and ID.
func getRecordsKey(tokenId []byte, name string) []byte { // This key always have a single corresponding value.
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) func getRecordKey(tokenId []byte, name string, typ RecordType, id byte) []byte {
return append(recordKey, getTokenKey([]byte(name))...) prefix := getRecordsByTypePrefix(tokenId, name, typ)
return append(prefix, id)
} }
// getRecordKey returns key used to store domain records. // getRecordsByTypePrefix returns prefix used to store domain records with the
func getRecordKey(tokenId []byte, name string, typ RecordType) []byte { // specified type of different IDs.
recordKey := getRecordsKey(tokenId, name) func getRecordsByTypePrefix(tokenId []byte, name string, typ RecordType) []byte {
recordKey := getRecordsPrefix(tokenId, name)
return append(recordKey, []byte{byte(typ)}...) 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. // isValid returns true if the provided address is a valid Uint160.
func isValid(address interop.Hash160) bool { func isValid(address interop.Hash160) bool {
return address != nil && len(address) == 20 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 // resolve resolves provided name using record with the specified type and given
// maximum redirections constraint. // 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 { if redirect < 0 {
panic("invalid redirect") panic("invalid redirect")
} }
@ -723,33 +772,33 @@ func resolve(ctx storage.Context, name string, typ RecordType, redirect int) str
if name[len(name)-1] == '.' { if name[len(name)-1] == '.' {
name = name[:len(name)-1] name = name[:len(name)-1]
} }
records := getRecords(ctx, name) records := getAllRecords(ctx, name)
cname := "" cname := ""
for iterator.Next(records) { for iterator.Next(records) {
r := iterator.Value(records).(struct { r := iterator.Value(records).(RecordState)
key string if r.Type == typ {
rs RecordState res = append(res, r.Data)
})
value := r.rs.Data
rTyp := r.key[len(r.key)-1]
if rTyp == byte(typ) {
return value
} }
if rTyp == byte(CNAME) { if r.Type == CNAME {
cname = value cname = r.Data
} }
} }
if cname == "" { if cname == "" || typ == CNAME {
return string([]byte(nil)) 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 // getAllRecords returns iterator over the set of records corresponded with the
// specified name. // specified name. Records returned are of different types and/or different IDs.
func getRecords(ctx storage.Context, name string) iterator.Iterator { // No keys are returned.
func getAllRecords(ctx storage.Context, name string) iterator.Iterator {
tokenID := []byte(tokenIDFromName(name)) tokenID := []byte(tokenIDFromName(name))
_ = getNameState(ctx, tokenID) _ = getNameState(ctx, tokenID) // ensure not expired.
recordsKey := getRecordsKey(tokenID, name) recordsPrefix := getRecordsPrefix(tokenID, name)
return storage.Find(ctx, recordsKey, storage.DeserializeValues) return storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
} }

View file

@ -2,7 +2,7 @@ name: "NameService"
sourceurl: https://github.com/nspcc-dev/neo-go/ sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: ["NEP-11"] supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords",
"resolve", "getAllRecords"] "resolve", "getAllRecords"]
events: events:
- name: Transfer - name: Transfer

View file

@ -1,6 +1,8 @@
package nns_test package nns_test
import ( import (
"math/big"
"strconv"
"strings" "strings"
"testing" "testing"
@ -100,7 +102,7 @@ func TestExpiration(t *testing.T) {
c.Invoke(t, true, "register", "com", c.CommitteeHash) c.Invoke(t, true, "register", "com", c.CommitteeHash)
cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash()) 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) b1 := e.TopBlock(t)
tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) 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))) require.NoError(t, bc.AddBlock(e.SignBlock(b4)))
e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) // TLD "com" has been expired 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 := e.NewUnsignedBlock(t, tx)
b5.Index = b4.Index + 1 b5.Index = b4.Index + 1
b5.PrevHash = b4.Hash() b5.PrevHash = b4.Hash()
@ -208,7 +210,7 @@ func TestRegisterAndRenew(t *testing.T) {
c.Invoke(t, props, "properties", "neo.com") c.Invoke(t, props, "properties", "neo.com")
} }
func TestSetGetRecord(t *testing.T) { func TestSetAddGetRecord(t *testing.T) {
c := newNSClient(t) c := newNSClient(t)
e := c.Executor e := c.Executor
@ -217,33 +219,56 @@ func TestSetGetRecord(t *testing.T) {
c.Invoke(t, true, "register", "com", c.CommitteeHash) c.Invoke(t, true, "register", "com", c.CommitteeHash)
t.Run("set before register", func(t *testing.T) { 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) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash)
t.Run("invalid parameters", func(t *testing.T) { 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, "unsupported record type", "addRecord", "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, "invalid record", "addRecord", "neo.com", int64(nns.A), "not.an.ip.address")
}) })
t.Run("invalid witness", func(t *testing.T) { 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.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") c.Invoke(t, stackitem.Null{}, "addRecord", "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.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record.
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") stackitem.Make("1.2.3.4"),
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") stackitem.Make("1.2.3.4"),
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") }), "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. // Delete record.
t.Run("invalid witness", func(t *testing.T) { 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.NewArray([]stackitem.Item{stackitem.Make("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "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("1.2.3.4"),
stackitem.Make("1.2.3.4"),
}), "getRecords", "neo.com", int64(nns.A))
t.Run("SetRecord_compatibility", func(t *testing.T) { t.Run("SetRecord_compatibility", func(t *testing.T) {
// tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior // 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} args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name}
t.Run(testCase.Name, func(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) {
if testCase.ShouldFail { if testCase.ShouldFail {
c.InvokeFail(t, "", "setRecord", args...) c.InvokeFail(t, "", "addRecord", args...)
} else { } 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") c.Invoke(t, props, "properties", "neo.com")
t.Run("set and delete by admin", func(t *testing.T) { t.Run("set and delete by admin", 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")
cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT))
cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "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) { 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) 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) c.Invoke(t, true, "register", "com", c.CommitteeHash)
cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash()) 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) cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil)
c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, true, "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) c.Invoke(t, true, "register", "com", c.CommitteeHash)
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) 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{}, "addRecord", "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.CNAME), "alias.com")
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) 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)) cAccCommittee.Invoke(t, true, "register", "alias2.com", acc.ScriptHash())
c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias2.com", int64(nns.TXT), "sometxt from alias2")
c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT))
c.Invoke(t, "sometxt", "resolve", "neo.com.", int64(nns.TXT)) 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.TXT)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com.", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) 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) { func TestGetAllRecords(t *testing.T) {
@ -474,14 +508,14 @@ func TestGetAllRecords(t *testing.T) {
c.Invoke(t, true, "register", "com", c.CommitteeHash) c.Invoke(t, true, "register", "com", c.CommitteeHash)
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) 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{}, "addRecord", "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.CNAME), "alias.com")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla0") cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "bla0")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla1") // overwrite cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 0, "bla1") // overwrite
// Add some arbitrary data. // Add some arbitrary data.
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) 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") script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com")
require.NoError(t, err) require.NoError(t, err)
@ -491,21 +525,63 @@ func TestGetAllRecords(t *testing.T) {
stackitem.NewByteArray([]byte("neo.com")), stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.A), stackitem.Make(nns.A),
stackitem.NewByteArray([]byte("1.2.3.4")), stackitem.NewByteArray([]byte("1.2.3.4")),
stackitem.NewBigInteger(big.NewInt(0)),
}), }),
stackitem.NewStruct([]stackitem.Item{ stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")), stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.CNAME), stackitem.Make(nns.CNAME),
stackitem.NewByteArray([]byte("alias.com")), stackitem.NewByteArray([]byte("alias.com")),
stackitem.NewBigInteger(big.NewInt(0)),
}), }),
stackitem.NewStruct([]stackitem.Item{ stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")), stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.TXT), stackitem.Make(nns.TXT),
stackitem.NewByteArray([]byte("bla1")), 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 ( const (
defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceDomainPrice = 10_0000_0000
defaultNameServiceSysfee = 6000_0000 defaultNameServiceSysfee = 6000_0000
maxRecordID = 255
) )

View file

@ -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)) t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID))
// Block #15: set A record type with priv0 owner via NNS. // 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 // 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 txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1