diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 8f67beecbc..d9e71799ce 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -47,14 +47,17 @@ const ( maxRegisterPrice = 1_0000_0000_0000 // maxRootLength is the maximum domain root length. maxRootLength = 16 - // maxDomainNameFragmentLength is the maximum length of the domain name fragment. - maxDomainNameFragmentLength = 62 + // maxDomainNameFragmentLength is the maximum length of the domain name fragment + maxDomainNameFragmentLength = 63 // minDomainNameLength is minimum domain length. minDomainNameLength = 3 // maxDomainNameLength is maximum domain length. 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. @@ -118,6 +122,7 @@ func Properties(tokenID []byte) map[string]interface{} { return map[string]interface{}{ "name": ns.Name, "expiration": ns.Expiration, + "admin": ns.Admin, } } @@ -179,22 +184,6 @@ func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool { return true } -// AddRoot registers new root. -func AddRoot(root string) { - checkCommittee() - if !checkFragment(root, true) { - panic("invalid root format") - } - var ( - ctx = storage.GetContext() - rootKey = append([]byte{prefixRoot}, []byte(root)...) - ) - if storage.Get(ctx, rootKey) != nil { - panic("root already exists") - } - storage.Put(ctx, rootKey, 0) -} - // Roots returns iterator over a set of NameService roots. func Roots() iterator.Iterator { ctx := storage.GetReadOnlyContext() @@ -224,15 +213,36 @@ func IsAvailable(name string) bool { panic("invalid domain name format") } ctx := storage.GetReadOnlyContext() - if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil { - panic("root not found") - } - nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...)) - if nsBytes == nil { + l := len(fragments) + if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[l-1])...)) == nil { + if l != 1 { + panic("TLD not found") + } return true } - ns := std.Deserialize(nsBytes.([]byte)).(NameState) - return runtime.GetTime() >= ns.Expiration + return parentExpired(ctx, 0, fragments) +} + +// parentExpired returns true if any domain from fragments doesn't exist or expired. +// first denotes the deepest subdomain to check. +func parentExpired(ctx storage.Context, first int, fragments []string) bool { + now := runtime.GetTime() + last := len(fragments) - 1 + name := fragments[last] + for i := last; i >= first; i-- { + if i != last { + name = fragments[i] + "." + name + } + nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...)) + if nsBytes == nil { + return true + } + ns := std.Deserialize(nsBytes.([]byte)).(NameState) + if now >= ns.Expiration { + return true + } + } + return false } // Register registers new domain with the specified owner and name if it's available. @@ -241,9 +251,27 @@ func Register(name string, owner interop.Hash160) bool { if fragments == nil { panic("invalid domain name format") } + l := len(fragments) + tldKey := append([]byte{prefixRoot}, []byte(fragments[l-1])...) ctx := storage.GetContext() - if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil { - panic("root not found") + tldBytes := storage.Get(ctx, tldKey) + if l == 1 { + checkCommittee() + if tldBytes != nil { + panic("TLD already exists") + } + storage.Put(ctx, tldKey, 0) + } else { + if tldBytes == nil { + panic("TLD not found") + } + if parentExpired(ctx, 1, fragments) { + panic("one of the parent domains has expired") + } + parentKey := getTokenKey([]byte(fragments[1])) + nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...)) + ns := std.Deserialize(nsBytes.([]byte)).(NameState) + ns.checkAdmin() } if !isValid(owner) { @@ -313,8 +341,43 @@ 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.ValuesOnly|storage.DeserializeValues) + for iterator.Next(records) { + r := iterator.Value(records).(RecordState) + if r.Name == name && r.Type == typ && r.Data == data { + panic("record already exists") + } + 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 { @@ -332,44 +395,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. @@ -425,7 +490,12 @@ func getTokenKey(tokenID []byte) []byte { // getNameState returns domain name state by the specified tokenID. func getNameState(ctx storage.Context, tokenID []byte) NameState { tokenKey := getTokenKey(tokenID) - return getNameStateWithKey(ctx, tokenKey) + ns := getNameStateWithKey(ctx, tokenKey) + fragments := std.StringSplit(string(tokenID), ".") + if parentExpired(ctx, 1, fragments) { + panic("parent domain has expired") + } + return ns } // getNameStateWithKey returns domain name state by the specified token key. @@ -453,41 +523,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 @@ -507,6 +589,8 @@ func checkCommittee() { } // checkFragment validates root or a part of domain name. +// 1. Root domain must start with a letter. +// 2. All other fragments must start and end in a letter or a digit. func checkFragment(v string, isRoot bool) bool { maxLength := maxDomainNameFragmentLength if isRoot { @@ -525,12 +609,12 @@ func checkFragment(v string, isRoot bool) bool { return false } } - for i := 1; i < len(v); i++ { - if !isAlNum(v[i]) { + for i := 1; i < len(v)-1; i++ { + if v[i] != '-' && !isAlNum(v[i]) { return false } } - return true + return isAlNum(v[len(v)-1]) } // isAlNum checks whether provided char is a lowercase letter or a number. @@ -546,9 +630,6 @@ func splitAndCheck(name string, allowMultipleFragments bool) []string { } fragments := std.StringSplit(name, ".") l = len(fragments) - if l < 2 { - return nil - } if l > 2 && !allowMultipleFragments { return nil } @@ -677,42 +758,51 @@ func tokenIDFromName(name string) string { panic("invalid domain name format") } l := len(fragments) + if l == 1 { + return name + } return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):] } // 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") } - records := getRecords(ctx, name) + if len(name) == 0 { + panic("invalid name") + } + if name[len(name)-1] == '.' { + name = name[:len(name)-1] + } + 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 1f24f3bc93..4c25081729 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 dcd0f6993d..6c2d44af89 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" @@ -9,6 +11,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" @@ -70,21 +73,21 @@ func TestNonfungible(t *testing.T) { c.Invoke(t, 0, "totalSupply") } -func TestAddRoot(t *testing.T) { +func TestRegisterTLD(t *testing.T) { c := newNSClient(t) t.Run("invalid format", func(t *testing.T) { - c.InvokeFail(t, "invalid root format", "addRoot", "") + c.InvokeFail(t, "invalid domain name format", "register", "", c.CommitteeHash) }) t.Run("not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) c := c.WithSigners(acc) - c.InvokeFail(t, "not witnessed by committee", "addRoot", "some") + c.InvokeFail(t, "not witnessed by committee", "register", "some", c.CommitteeHash) }) - c.Invoke(t, stackitem.Null{}, "addRoot", "some") + c.Invoke(t, true, "register", "some", c.CommitteeHash) t.Run("already exists", func(t *testing.T) { - c.InvokeFail(t, "already exists", "addRoot", "some") + c.InvokeFail(t, "TLD already exists", "register", "some", c.CommitteeHash) }) } @@ -95,19 +98,20 @@ func TestExpiration(t *testing.T) { acc := e.NewAccount(t) cAcc := c.WithSigners(acc) + cAccCommittee := c.WithSigners(acc, c.Committee) // acc + committee signers for ".com"'s subdomains registration - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") + c.Invoke(t, true, "register", "com", c.CommitteeHash) + cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) - tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) + tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) b2 := e.NewUnsignedBlock(t, tx) b2.Index = b1.Index + 1 b2.PrevHash = b1.Hash() b2.Timestamp = b1.Timestamp + 10000 require.NoError(t, bc.AddBlock(e.SignBlock(b2))) - e.CheckHalt(t, tx.Hash()) + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com") b3 := e.NewUnsignedBlock(t, tx) @@ -115,7 +119,7 @@ func TestExpiration(t *testing.T) { b3.PrevHash = b2.Hash() b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1) require.NoError(t, bc.AddBlock(e.SignBlock(b3))) - e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) + e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) // "first.com" has been expired tx = cAcc.PrepareInvoke(t, "isAvailable", "second.com") b4 := e.NewUnsignedBlock(t, tx) @@ -123,9 +127,9 @@ func TestExpiration(t *testing.T) { b4.PrevHash = b3.Hash() b4.Timestamp = b3.Timestamp + 1000 require.NoError(t, bc.AddBlock(e.SignBlock(b4))) - e.CheckHalt(t, tx.Hash(), stackitem.NewBool(false)) + 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() @@ -133,20 +137,27 @@ func TestExpiration(t *testing.T) { require.NoError(t, bc.AddBlock(e.SignBlock(b5))) e.CheckFault(t, tx.Hash(), "name has expired") - cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) // Re-register. - cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT)) + // TODO: According to the new code, we can't re-register expired "com" TLD, because it's already registered; at the + // same time we can't renew it because it's already expired. We likely need to change this logic in the contract and + // after that uncomment the lines below. + // c.Invoke(t, true, "renew", "com") + // cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) // Re-register. + // cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT)) } -const millisecondsInYear = 365 * 24 * 3600 * 1000 +const ( + millisecondsInYear = 365 * 24 * 3600 * 1000 + maxDomainNameFragmentLength = 63 +) func TestRegisterAndRenew(t *testing.T) { c := newNSClient(t) e := c.Executor - c.InvokeFail(t, "root not found", "isAvailable", "neo.com") - c.Invoke(t, stackitem.Null{}, "addRoot", "org") - c.InvokeFail(t, "root not found", "isAvailable", "neo.com") - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com") + c.Invoke(t, true, "register", "org", c.CommitteeHash) + c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com") + c.Invoke(t, true, "register", "com", c.CommitteeHash) c.Invoke(t, true, "isAvailable", "neo.com") c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash) @@ -154,20 +165,34 @@ func TestRegisterAndRenew(t *testing.T) { c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash) + var maxLenFragment string + for i := 0; i < maxDomainNameFragmentLength; i++ { + maxLenFragment += "q" + } + c.Invoke(t, true, "isAvailable", maxLenFragment+".com") + c.Invoke(t, true, "register", maxLenFragment+".com", e.CommitteeHash) + c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash) c.Invoke(t, true, "isAvailable", "neo.com") - c.Invoke(t, 0, "balanceOf", e.CommitteeHash) + c.Invoke(t, 3, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) topBlock := e.TopBlock(t) expectedExpiration := topBlock.Timestamp + millisecondsInYear c.Invoke(t, false, "register", "neo.com", e.CommitteeHash) c.Invoke(t, false, "isAvailable", "neo.com") + t.Run("domain names with hyphen", func(t *testing.T) { + c.InvokeFail(t, "invalid domain name format", "register", "-testdomain.com", e.CommitteeHash) + c.InvokeFail(t, "invalid domain name format", "register", "testdomain-.com", e.CommitteeHash) + c.Invoke(t, true, "register", "test-domain.com", e.CommitteeHash) + }) + props := stackitem.NewMap() props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) + props.Add(stackitem.Make("admin"), stackitem.Null{}) // no admin was set c.Invoke(t, props, "properties", "neo.com") - c.Invoke(t, 1, "balanceOf", e.CommitteeHash) + c.Invoke(t, 5, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com, neo.com, test-domain.com c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) t.Run("invalid token ID", func(t *testing.T) { @@ -185,42 +210,59 @@ 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 acc := e.NewAccount(t) cAcc := c.WithSigners(acc) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + 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.InvokeFail(t, "record already exists", "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")}), "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")}), "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 @@ -280,9 +322,10 @@ 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...) + c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(testCase.Type)) // clear records after test to avoid duplicating records. } }) } @@ -295,14 +338,17 @@ func TestSetAdmin(t *testing.T) { owner := e.NewAccount(t) cOwner := c.WithSigners(owner) + cOwnerCommittee := c.WithSigners(owner, c.Committee) admin := e.NewAccount(t) cAdmin := c.WithSigners(admin) guest := e.NewAccount(t) cGuest := c.WithSigners(guest) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + c.Invoke(t, true, "register", "com", c.CommitteeHash) - cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash()) + cOwner.InvokeFail(t, "not witnessed by admin", "register", "neo.com", owner.ScriptHash()) // admin is committee + cOwnerCommittee.Invoke(t, true, "register", "neo.com", owner.ScriptHash()) + expectedExpiration := e.TopBlock(t).Timestamp + millisecondsInYear cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) // Must be witnessed by both owner and admin. @@ -310,17 +356,22 @@ func TestSetAdmin(t *testing.T) { cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash()) cc := c.WithSigners(owner, admin) cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) + props := stackitem.NewMap() + props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) + props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) + props.Add(stackitem.Make("admin"), stackitem.Make(admin.ScriptHash().BytesBE())) + 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)) }) } @@ -330,16 +381,17 @@ func TestTransfer(t *testing.T) { from := e.NewAccount(t) cFrom := c.WithSigners(from) + cFromCommittee := c.WithSigners(from, c.Committee) to := e.NewAccount(t) cTo := c.WithSigners(to) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash()) - cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, true, "register", "com", c.CommitteeHash) + cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash()) + 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) - cFrom.Invoke(t, 1, "totalSupply") + cFrom.Invoke(t, 2, "totalSupply") // com, neo.com cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com") // without onNEP11Transfer @@ -358,7 +410,7 @@ func TestTransfer(t *testing.T) { &compiler.Options{Name: "foo"}) e.DeployContract(t, ctr, nil) cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil) - cFrom.Invoke(t, 1, "totalSupply") + cFrom.Invoke(t, 2, "totalSupply") // com, neo.com cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com")) } @@ -367,21 +419,22 @@ func TestTokensOf(t *testing.T) { e := c.Executor acc1 := e.NewAccount(t) - cAcc1 := c.WithSigners(acc1) + cAcc1Committee := c.WithSigners(acc1, c.Committee) acc2 := e.NewAccount(t) - cAcc2 := c.WithSigners(acc2) + cAcc2Committee := c.WithSigners(acc2, c.Committee) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) - cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) + tld := []byte("com") + c.Invoke(t, true, "register", tld, c.CommitteeHash) + cAcc1Committee.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) + cAcc2Committee.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) - testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) - testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) - testTokensOf(t, c, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) - testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still + testTokensOf(t, c, tld, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) + testTokensOf(t, c, tld, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) + testTokensOf(t, c, tld, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) + testTokensOf(t, c, tld, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still } -func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, args ...interface{}) { +func testTokensOf(t *testing.T, c *neotest.ContractInvoker, tld []byte, result [][]byte, args ...interface{}) { method := "tokensOf" if len(args) == 0 { method = "tokens" @@ -399,7 +452,12 @@ func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, arg require.Equal(t, result[i], iter.Value().Value()) arr = append(arr, stackitem.Make(result[i])) } - require.False(t, iter.Next()) + if method == "tokens" { + require.True(t, iter.Next()) + require.Equal(t, tld, iter.Value().Value()) + } else { + require.False(t, iter.Next()) + } } func TestResolve(t *testing.T) { @@ -408,22 +466,117 @@ func TestResolve(t *testing.T) { 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") + + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) + 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") + + 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) { + 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") + 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{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt") + + script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com") + require.NoError(t, err) + h := e.InvokeScript(t, script, []neotest.Signer{acc}) + e.CheckHalt(t, h, stackitem.NewArray([]stackitem.Item{ + stackitem.NewStruct([]stackitem.Item{ + 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") - c.Invoke(t, stackitem.Null{}, "addRoot", "com") - cAcc.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") + // 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") - cAcc.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "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)) +} - 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, stackitem.Null{}, "resolve", "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 f0071cfd39..7dff03d7bb 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -158,16 +158,17 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) { _, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, nsConfigPath, 4) // block #11 nsCommitteeInvoker := e.CommitteeInvoker(nsHash) nsPriv0Invoker := e.NewInvoker(nsHash, acc0) + nsPriv0CommitteeInvoker := e.NewInvoker(nsHash, acc0, e.Committee) // Block #12: transfer funds to committee for further NS record registration. gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12 // Block #13: add `.com` root to NNS. - nsCommitteeInvoker.Invoke(t, stackitem.Null{}, "addRoot", "com") // block #13 + nsCommitteeInvoker.Invoke(t, true, "register", "com", nsCommitteeInvoker.CommitteeHash) // block #13 // Block #14: register `neo.com` via NNS. - registerTxH := nsPriv0Invoker.Invoke(t, true, "register", + registerTxH := nsPriv0CommitteeInvoker.Invoke(t, true, "register", "neo.com", priv0ScriptHash) // block #14 res := e.GetTxExecResult(t, registerTxH) require.Equal(t, 1, len(res.Events)) // transfer @@ -176,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 diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index dc7fb11191..be3a23994e 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1370,7 +1370,7 @@ func TestClient_NEP11_ND(t *testing.T) { t.Run("TotalSupply", func(t *testing.T) { s, err := n11.TotalSupply() require.NoError(t, err) - require.EqualValues(t, big.NewInt(1), s) // the only `neo.com` of acc0 + require.EqualValues(t, big.NewInt(2), s) // `neo.com` of acc0 and TLD `com` of committee }) t.Run("Symbol", func(t *testing.T) { sym, err := n11.Symbol() @@ -1403,14 +1403,14 @@ func TestClient_NEP11_ND(t *testing.T) { require.NoError(t, err) items, err := iter.Next(config.DefaultMaxIteratorResultItems) require.NoError(t, err) - require.Equal(t, 1, len(items)) - require.Equal(t, [][]byte{[]byte("neo.com")}, items) + require.Equal(t, 2, len(items)) + require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items) require.NoError(t, iter.Terminate()) }) t.Run("TokensExpanded", func(t *testing.T) { items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems) require.NoError(t, err) - require.Equal(t, [][]byte{[]byte("neo.com")}, items) + require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items) }) t.Run("Properties", func(t *testing.T) { p, err := n11.Properties([]byte("neo.com")) @@ -1421,6 +1421,7 @@ func TestClient_NEP11_ND(t *testing.T) { expected := stackitem.NewMap() expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com"))) expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula + expected.Add(stackitem.Make([]byte("admin")), stackitem.Null{}) require.EqualValues(t, expected, p) }) t.Run("Transfer", func(t *testing.T) { diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index d5a9fce4a8..903f0db922 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -74,12 +74,12 @@ const ( verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c" verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781" - nnsContractHash = "bdbfe1a280a0e23ca5b569c8f5845169bd93cb06" + nnsContractHash = "cb93bcab0d6d435b61fa96a3bbce3b6f043968b5" nnsToken1ID = "6e656f2e636f6d" nfsoContractHash = "0e15ca0df00669a2cd5dcb03bfd3e2b3849c2969" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" - block20StateRootLE = "f1380226a217b5e35ea968d42c50e20b9af7ab83b91416c8fb85536c61004332" + block20StateRootLE = "7f80c7e265a44faa7374953d4d5059d21b34e65e06a7695d57ca8c59cc9a36fa" storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7" ) @@ -287,6 +287,7 @@ var rpcTestCases = map[string][]rpcTestCase{ return &map[string]interface{}{ "name": "neo.com", "expiration": "lhbLRl0B", + "admin": nil, // no admin was set } }, }, @@ -935,7 +936,7 @@ var rpcTestCases = map[string][]rpcTestCase{ chg := []dboper.Operation{{ State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, - Value: []byte{0xf6, 0x8b, 0x4e, 0x9d, 0x51, 0x79, 0x12}, + Value: []byte{0x6e, 0xaf, 0xba, 0x5e, 0x51, 0x79, 0x12}, }, { State: "Added", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, @@ -947,7 +948,7 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x01, 0x21, 0x05, 0xe4, 0x74, 0xef, 0xdb, 0x08}, + Value: []byte{0x41, 0x01, 0x21, 0x05, 0xda, 0xb5, 0x8c, 0xda, 0x08}, }} // Can be returned in any order. assert.ElementsMatch(t, chg, res.Diagnostics.Changes) @@ -963,7 +964,7 @@ var rpcTestCases = map[string][]rpcTestCase{ cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 15928320, + GasConsumed: 22192980, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -975,6 +976,15 @@ var rpcTestCases = map[string][]rpcTestCase{ { Current: nnsHash, Calls: []*invocations.Tree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, { Current: stdHash, }, @@ -1078,7 +1088,7 @@ var rpcTestCases = map[string][]rpcTestCase{ cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 15928320, + GasConsumed: 22192980, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -1090,6 +1100,15 @@ var rpcTestCases = map[string][]rpcTestCase{ { Current: nnsHash, Calls: []*invocations.Tree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, { Current: stdHash, }, @@ -2717,7 +2736,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "37099660700", + Amount: "37076412050", LastUpdated: 22, Decimals: 8, Name: "GasToken", diff --git a/pkg/services/rpcsrv/testdata/testblocks.acc b/pkg/services/rpcsrv/testdata/testblocks.acc index 965230d6a4..2fcfdc437e 100644 Binary files a/pkg/services/rpcsrv/testdata/testblocks.acc and b/pkg/services/rpcsrv/testdata/testblocks.acc differ