From bd3722041a43fb9ac85c79517e903472a3d27cd7 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:30:38 +0300 Subject: [PATCH 01/11] nns: adjust maxDomainNameFragmentLength Port https://github.com/nspcc-dev/neofs-contract/pull/238. --- examples/nft-nd-nns/nns.go | 4 ++-- examples/nft-nd-nns/nns_test.go | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 8f67beecbc..d5039d37ba 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -47,8 +47,8 @@ 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. diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index dcd0f6993d..1e80ed7ce3 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -137,7 +137,10 @@ func TestExpiration(t *testing.T) { 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) @@ -154,9 +157,16 @@ 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, 1, "balanceOf", e.CommitteeHash) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) topBlock := e.TopBlock(t) expectedExpiration := topBlock.Timestamp + millisecondsInYear @@ -167,7 +177,7 @@ func TestRegisterAndRenew(t *testing.T) { props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) c.Invoke(t, props, "properties", "neo.com") - c.Invoke(t, 1, "balanceOf", e.CommitteeHash) + c.Invoke(t, 2, "balanceOf", e.CommitteeHash) c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) t.Run("invalid token ID", func(t *testing.T) { From c11481b119a60920e71e26ab5e5429bc568aa702 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:30:43 +0300 Subject: [PATCH 02/11] nns: allow hyphen in domain names Port https://github.com/nspcc-dev/neofs-contract/pull/183. --- examples/nft-nd-nns/nns.go | 8 +++++--- examples/nft-nd-nns/nns_test.go | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index d5039d37ba..f04c5525b3 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -507,6 +507,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 +527,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. diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 1e80ed7ce3..230aa72020 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -173,6 +173,12 @@ func TestRegisterAndRenew(t *testing.T) { 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)) From 5cb2a1219c2df8813a0fdb509b091806b3ec2580 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:30:47 +0300 Subject: [PATCH 03/11] nns: replace root with TLD Port https://github.com/nspcc-dev/neofs-contract/pull/139/commits/4b86891d57ef12c3f37a9ef27902c73d96812934. --- examples/nft-nd-nns/nns.go | 75 +++++++++++++++++++++------------ examples/nft-nd-nns/nns_test.go | 70 +++++++++++++++++------------- internal/basicchain/basic.go | 2 +- 3 files changed, 88 insertions(+), 59 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index f04c5525b3..c1df13dca3 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -179,22 +179,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 +208,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 +246,23 @@ 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") + } } if !isValid(owner) { @@ -548,9 +567,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 } @@ -679,6 +695,9 @@ 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):] } diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 230aa72020..bb7edd9acf 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -70,21 +70,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) }) } @@ -96,7 +96,7 @@ func TestExpiration(t *testing.T) { acc := e.NewAccount(t) cAcc := c.WithSigners(acc) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + c.Invoke(t, true, "register", "com", c.CommitteeHash) cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) @@ -107,7 +107,7 @@ func TestExpiration(t *testing.T) { 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 +115,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,7 +123,7 @@ 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)) b5 := e.NewUnsignedBlock(t, tx) @@ -133,8 +133,12 @@ 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 ( @@ -146,10 +150,10 @@ 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) @@ -166,7 +170,7 @@ func TestRegisterAndRenew(t *testing.T) { c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash) c.Invoke(t, true, "isAvailable", "neo.com") - c.Invoke(t, 1, "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 @@ -183,7 +187,7 @@ func TestRegisterAndRenew(t *testing.T) { props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) c.Invoke(t, props, "properties", "neo.com") - c.Invoke(t, 2, "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) { @@ -207,7 +211,7 @@ func TestSetGetRecord(t *testing.T) { 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") @@ -316,7 +320,7 @@ func TestSetAdmin(t *testing.T) { 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()) cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) @@ -349,13 +353,13 @@ func TestTransfer(t *testing.T) { to := e.NewAccount(t) cTo := c.WithSigners(to) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + c.Invoke(t, true, "register", "com", c.CommitteeHash) cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash()) cFrom.Invoke(t, stackitem.Null{}, "setRecord", "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 @@ -374,7 +378,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")) } @@ -387,17 +391,18 @@ func TestTokensOf(t *testing.T) { acc2 := e.NewAccount(t) cAcc2 := c.WithSigners(acc2) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + tld := []byte("com") + c.Invoke(t, true, "register", tld, c.CommitteeHash) cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) cAcc2.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" @@ -415,7 +420,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) { @@ -425,7 +435,7 @@ func TestResolve(t *testing.T) { acc := e.NewAccount(t) cAcc := c.WithSigners(acc) - c.Invoke(t, stackitem.Null{}, "addRoot", "com") + c.Invoke(t, true, "register", "com", c.CommitteeHash) 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") diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index f0071cfd39..8cf016f1f4 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -164,7 +164,7 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) { 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", From 017a6b9bc1ede351f205209474c2d9e6e9365819 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:30:54 +0300 Subject: [PATCH 04/11] nns: require admin signature for subdomain registration Port https://github.com/nspcc-dev/neofs-contract/pull/139/commits/14fc08629180e9d53d7efe431fb20b245c6ecf78. --- examples/nft-nd-nns/nns.go | 4 ++++ examples/nft-nd-nns/nns_test.go | 25 +++++++++++++++---------- internal/basicchain/basic.go | 3 ++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index c1df13dca3..f9aaadb447 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -263,6 +263,10 @@ func Register(name string, owner interop.Hash160) bool { 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) { diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index bb7edd9acf..983db3474d 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -95,13 +95,14 @@ 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, true, "register", "com", c.CommitteeHash) - cAcc.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") 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() @@ -315,6 +316,7 @@ 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) @@ -322,7 +324,8 @@ func TestSetAdmin(t *testing.T) { 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()) cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) // Must be witnessed by both owner and admin. @@ -350,11 +353,12 @@ 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, true, "register", "com", c.CommitteeHash) - cFrom.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.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) @@ -387,14 +391,14 @@ 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) tld := []byte("com") c.Invoke(t, true, "register", tld, c.CommitteeHash) - cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) - cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) + cAcc1Committee.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) + cAcc2Committee.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) testTokensOf(t, c, tld, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) testTokensOf(t, c, tld, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) @@ -434,13 +438,14 @@ 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) - cAcc.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{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") - cAcc.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") c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index 8cf016f1f4..f6683bf2fd 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -158,6 +158,7 @@ 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", @@ -167,7 +168,7 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) { 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 From baf24d1c66c5e91f73af4e5b5410202019f82a8e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:30:58 +0300 Subject: [PATCH 05/11] nns: check domain expiration for read functions Port https://github.com/nspcc-dev/neofs-contract/pull/139/commits/432c02a3696e42a03863dbe3fae2a889ec8224e6. --- examples/nft-nd-nns/nns.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index f9aaadb447..90ffa08cf4 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -448,7 +448,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. From 225152f2d723e22e57fe0e11976cfb4ea50c3d5d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:31:00 +0300 Subject: [PATCH 06/11] nns: allow to resolve FQDN Port https://github.com/nspcc-dev/neofs-contract/pull/139/commits/4041924a75a7d292b2e42b83f5795d7cb961bcba. --- examples/nft-nd-nns/nns.go | 6 ++++++ examples/nft-nd-nns/nns_test.go | 2 ++ 2 files changed, 8 insertions(+) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 90ffa08cf4..0236dcd6a0 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -716,6 +716,12 @@ func resolve(ctx storage.Context, name string, typ RecordType, redirect int) str if redirect < 0 { panic("invalid redirect") } + if len(name) == 0 { + panic("invalid name") + } + if name[len(name)-1] == '.' { + name = name[:len(name)-1] + } records := getRecords(ctx, name) cname := "" for iterator.Next(records) { diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 983db3474d..e4e9bf21bc 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -451,6 +451,8 @@ func TestResolve(t *testing.T) { c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT)) + c.Invoke(t, "sometxt", "resolve", "neo.com.", int64(nns.TXT)) + c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.TXT)) c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) } From d77b35c38503b91205e8f4b1598c646f1386b109 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:31:03 +0300 Subject: [PATCH 07/11] nns: add admin to properties See https://github.com/neo-project/non-native-contracts/blob/14f43ba8cf169323b61c23a3a701ac77d9a4e3eb/src/NameService/NameService.cs#L69. --- examples/nft-nd-nns/nns.go | 1 + examples/nft-nd-nns/nns_test.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 0236dcd6a0..69ed6d5a63 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -118,6 +118,7 @@ func Properties(tokenID []byte) map[string]interface{} { return map[string]interface{}{ "name": ns.Name, "expiration": ns.Expiration, + "admin": ns.Admin, } } diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index e4e9bf21bc..e292a8d44d 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -187,6 +187,7 @@ func TestRegisterAndRenew(t *testing.T) { 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, 5, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com, neo.com, test-domain.com c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) @@ -326,6 +327,7 @@ func TestSetAdmin(t *testing.T) { 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. @@ -333,6 +335,11 @@ 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") From 4543de0923fcb92b493042dbf998057705493e16 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 6 Sep 2022 13:31:20 +0300 Subject: [PATCH 08/11] *: update basic test chain Apply new NNS rules. --- pkg/services/rpcsrv/client_test.go | 9 +++--- pkg/services/rpcsrv/server_test.go | 33 +++++++++++++++----- pkg/services/rpcsrv/testdata/testblocks.acc | Bin 35080 -> 35871 bytes 3 files changed, 31 insertions(+), 11 deletions(-) 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 965230d6a41c18d15ae5096cba629b1e7f2bda78..2fcfdc437ee892ed3d3cda3e39f247a60343b4df 100644 GIT binary patch delta 5889 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 delta 5208 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 From c296f8804cad9e484068636d76fd937ae0504011 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:35:54 +0300 Subject: [PATCH 09/11] nns: add test for getAllRecords --- examples/nft-nd-nns/nns_test.go | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index e292a8d44d..39611b87ee 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -9,6 +9,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" @@ -463,6 +464,47 @@ func TestResolve(t *testing.T) { c.Invoke(t, stackitem.Null{}, "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{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla0") + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla1") // overwrite + + // Add some arbitrary data. + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") + + 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.NewStruct([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.Make(nns.CNAME), + stackitem.NewByteArray([]byte("alias.com")), + }), + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.Make(nns.TXT), + stackitem.NewByteArray([]byte("bla1")), + }), + })) +} + const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 From c9050cef4b6c25675693c0ca9f179a52f0a1c99a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:36:13 +0300 Subject: [PATCH 10/11] nns: allow multiple records of the same type Except for the CNAME records. Port https://github.com/nspcc-dev/neofs-contract/pull/133/commits/6ea4573ef86c445709c792f4b40c7ae200e7d799 and https://github.com/nspcc-dev/neofs-contract/pull/133/commits/f4762c1b5643382199fe3795a345ac6ba0cb1727. --- examples/nft-nd-nns/nns.go | 167 +++++++++++++++++++++----------- examples/nft-nd-nns/nns.yml | 2 +- examples/nft-nd-nns/nns_test.go | 162 +++++++++++++++++++++++-------- internal/basicchain/basic.go | 2 +- 4 files changed, 229 insertions(+), 104 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 69ed6d5a63..74ac349ab7 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -55,6 +55,9 @@ const ( maxDomainNameLength = 255 // maxTXTRecordLength is the maximum length of the TXT domain record. maxTXTRecordLength = 255 + // maxRecordID is the maximum value of record ID (the upper bound for the number + // of records with the same type). + maxRecordID = 255 ) // Other constants. @@ -70,6 +73,7 @@ type RecordState struct { Name string Type RecordType Data string + ID byte } // Update updates NameService contract. @@ -337,8 +341,39 @@ func SetAdmin(name string, admin interop.Hash160) { putNameState(ctx, ns) } -// SetRecord adds new record of the specified type to the provided domain. -func SetRecord(name string, typ RecordType, data string) { +// SetRecord updates record of the specified type and ID. +func SetRecord(name string, typ RecordType, id byte, data string) { + ctx := storage.GetContext() + tokenID := checkRecord(ctx, name, typ, data) + recordKey := getRecordKey(tokenID, name, typ, id) + recBytes := storage.Get(ctx, recordKey) + if recBytes == nil { + panic("unknown record") + } + putRecord(ctx, tokenID, name, typ, id, data) +} + +// AddRecord adds new record of the specified type to the provided domain. +func AddRecord(name string, typ RecordType, data string) { + ctx := storage.GetContext() + tokenID := checkRecord(ctx, name, typ, data) + recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) + var id byte + records := storage.Find(ctx, recordsPrefix, storage.KeysOnly) + for iterator.Next(records) { + id++ + } + if id > maxRecordID { + panic("maximum number of records reached") + } + if typ == CNAME && id != 0 { + panic("multiple CNAME records") + } + putRecord(ctx, tokenID, name, typ, id, data) +} + +// checkRecord performs record validness check and returns token ID. +func checkRecord(ctx storage.Context, name string, typ RecordType, data string) []byte { tokenID := []byte(tokenIDFromName(name)) var ok bool switch typ { @@ -356,44 +391,46 @@ func SetRecord(name string, typ RecordType, data string) { if !ok { panic("invalid record data") } - ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - putRecord(ctx, tokenID, name, typ, data) + return tokenID } -// GetRecord returns domain record of the specified type if it exists or an empty -// string if not. -func GetRecord(name string, typ RecordType) string { +// GetRecords returns domain records of the specified type if they exist or an empty +// array if not. +func GetRecords(name string, typ RecordType) []string { tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() _ = getNameState(ctx, tokenID) // ensure not expired - return getRecord(ctx, tokenID, name, typ) + return getRecordsByType(ctx, tokenID, name, typ) } -// DeleteRecord removes domain record with the specified type. -func DeleteRecord(name string, typ RecordType) { +// DeleteRecords removes all domain records with the specified type. +func DeleteRecords(name string, typ RecordType) { tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext() ns := getNameState(ctx, tokenID) ns.checkAdmin() - recordKey := getRecordKey(tokenID, name, typ) - storage.Delete(ctx, recordKey) + recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) + records := storage.Find(ctx, recordsPrefix, storage.KeysOnly) + for iterator.Next(records) { + key := iterator.Value(records).(string) + storage.Delete(ctx, key) + } } -// Resolve resolves given name (not more then three redirects are allowed). -func Resolve(name string, typ RecordType) string { +// Resolve resolves given name (not more than three redirects are allowed) to a set +// of domain records. +func Resolve(name string, typ RecordType) []string { ctx := storage.GetReadOnlyContext() - return resolve(ctx, name, typ, 2) + res := []string{} + return resolve(ctx, res, name, typ, 2) } // GetAllRecords returns an Iterator with RecordState items for given name. func GetAllRecords(name string) iterator.Iterator { - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetReadOnlyContext() - _ = getNameState(ctx, tokenID) // ensure not expired - recordsKey := getRecordsKey(tokenID, name) - return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) + return getAllRecords(ctx, name) } // updateBalance updates account's balance and account's tokens. @@ -482,41 +519,53 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) { storage.Put(ctx, nameKey, nsBytes) } -// getRecord returns domain record. -func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string { - recordKey := getRecordKey(tokenId, name, typ) - recBytes := storage.Get(ctx, recordKey) - if recBytes == nil { - return recBytes.(string) // A hack to actually return NULL. +// getRecordsByType returns domain records of the specified type or an empty array if no records found. +func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string { + recordsPrefix := getRecordsByTypePrefix(tokenId, name, typ) + records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues) + res := []string{} // return empty slice if no records was found. + for iterator.Next(records) { + r := iterator.Value(records).(RecordState) + if r.Type == typ { + res = append(res, r.Data) + } } - record := std.Deserialize(recBytes.([]byte)).(RecordState) - return record.Data + return res } -// putRecord stores domain record. -func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) { - recordKey := getRecordKey(tokenId, name, typ) +// putRecord puts the specified record to the contract storage without any additional checks. +func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) { + recordKey := getRecordKey(tokenId, name, typ, id) rs := RecordState{ Name: name, Type: typ, - Data: record, + Data: data, + ID: id, } recBytes := std.Serialize(rs) storage.Put(ctx, recordKey, recBytes) } -// getRecordsKey returns prefix used to store domain records of different types. -func getRecordsKey(tokenId []byte, name string) []byte { - recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) - return append(recordKey, getTokenKey([]byte(name))...) +// getRecordKey returns key used to store domain record with the specified type and ID. +// This key always have a single corresponding value. +func getRecordKey(tokenId []byte, name string, typ RecordType, id byte) []byte { + prefix := getRecordsByTypePrefix(tokenId, name, typ) + return append(prefix, id) } -// getRecordKey returns key used to store domain records. -func getRecordKey(tokenId []byte, name string, typ RecordType) []byte { - recordKey := getRecordsKey(tokenId, name) +// getRecordsByTypePrefix returns prefix used to store domain records with the +// specified type of different IDs. +func getRecordsByTypePrefix(tokenId []byte, name string, typ RecordType) []byte { + recordKey := getRecordsPrefix(tokenId, name) return append(recordKey, []byte{byte(typ)}...) } +// getRecordsPrefix returns prefix used to store domain records of different types. +func getRecordsPrefix(tokenId []byte, name string) []byte { + recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) + return append(recordKey, getTokenKey([]byte(name))...) +} + // isValid returns true if the provided address is a valid Uint160. func isValid(address interop.Hash160) bool { return address != nil && len(address) == 20 @@ -713,7 +762,7 @@ func tokenIDFromName(name string) string { // resolve resolves provided name using record with the specified type and given // maximum redirections constraint. -func resolve(ctx storage.Context, name string, typ RecordType, redirect int) string { +func resolve(ctx storage.Context, res []string, name string, typ RecordType, redirect int) []string { if redirect < 0 { panic("invalid redirect") } @@ -723,33 +772,33 @@ func resolve(ctx storage.Context, name string, typ RecordType, redirect int) str if name[len(name)-1] == '.' { name = name[:len(name)-1] } - records := getRecords(ctx, name) + records := getAllRecords(ctx, name) cname := "" for iterator.Next(records) { - r := iterator.Value(records).(struct { - key string - rs RecordState - }) - value := r.rs.Data - rTyp := r.key[len(r.key)-1] - if rTyp == byte(typ) { - return value + r := iterator.Value(records).(RecordState) + if r.Type == typ { + res = append(res, r.Data) } - if rTyp == byte(CNAME) { - cname = value + if r.Type == CNAME { + cname = r.Data } } - if cname == "" { - return string([]byte(nil)) + if cname == "" || typ == CNAME { + return res } - return resolve(ctx, cname, typ, redirect-1) + + // TODO: the line below must be removed from the neofs nns: + // res = append(res, cname) + // @roman-khimov, it is done in a separate commit in neofs-contracts repo, is it OK? + return resolve(ctx, res, cname, typ, redirect-1) } -// getRecords returns iterator over the set of records corresponded with the -// specified name. -func getRecords(ctx storage.Context, name string) iterator.Iterator { +// getAllRecords returns iterator over the set of records corresponded with the +// specified name. Records returned are of different types and/or different IDs. +// No keys are returned. +func getAllRecords(ctx storage.Context, name string) iterator.Iterator { tokenID := []byte(tokenIDFromName(name)) - _ = getNameState(ctx, tokenID) - recordsKey := getRecordsKey(tokenID, name) - return storage.Find(ctx, recordsKey, storage.DeserializeValues) + _ = getNameState(ctx, tokenID) // ensure not expired. + recordsPrefix := getRecordsPrefix(tokenID, name) + return storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues) } diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 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 39611b87ee..664b08aafa 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -1,6 +1,8 @@ package nns_test import ( + "math/big" + "strconv" "strings" "testing" @@ -100,7 +102,7 @@ func TestExpiration(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) @@ -127,7 +129,7 @@ func TestExpiration(t *testing.T) { require.NoError(t, bc.AddBlock(e.SignBlock(b4))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) // TLD "com" has been expired - tx = cAcc.PrepareInvoke(t, "getRecord", "first.com", int64(nns.TXT)) + tx = cAcc.PrepareInvoke(t, "getRecords", "first.com", int64(nns.TXT)) b5 := e.NewUnsignedBlock(t, tx) b5.Index = b4.Index + 1 b5.PrevHash = b4.Hash() @@ -208,7 +210,7 @@ func TestRegisterAndRenew(t *testing.T) { c.Invoke(t, props, "properties", "neo.com") } -func TestSetGetRecord(t *testing.T) { +func TestSetAddGetRecord(t *testing.T) { c := newNSClient(t) e := c.Executor @@ -217,33 +219,56 @@ func TestSetGetRecord(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) t.Run("set before register", func(t *testing.T) { - c.InvokeFail(t, "token not found", "setRecord", "neo.com", int64(nns.TXT), "sometext") + c.InvokeFail(t, "token not found", "addRecord", "neo.com", int64(nns.TXT), "sometext") }) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) t.Run("invalid parameters", func(t *testing.T) { - c.InvokeFail(t, "unsupported record type", "setRecord", "neo.com", int64(0xFF), "1.2.3.4") - c.InvokeFail(t, "invalid record", "setRecord", "neo.com", int64(nns.A), "not.an.ip.address") + c.InvokeFail(t, "unsupported record type", "addRecord", "neo.com", int64(0xFF), "1.2.3.4") + c.InvokeFail(t, "invalid record", "addRecord", "neo.com", int64(nns.A), "not.an.ip.address") }) t.Run("invalid witness", func(t *testing.T) { - cAcc.InvokeFail(t, "not witnessed by admin", "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.InvokeFail(t, "not witnessed by admin", "addRecord", "neo.com", int64(nns.A), "1.2.3.4") }) - c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") - c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("1.2.3.4"), + stackitem.Make("1.2.3.4"), + }), "getRecords", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") + // Add multiple records and update some of them. + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext1") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext2") + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext3") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("sometext"), + stackitem.Make("sometext1"), + stackitem.Make("sometext2"), + stackitem.Make("sometext3"), + }), "getRecords", "neo.com", int64(nns.TXT)) + c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 2, "sometext22") + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("sometext"), + stackitem.Make("sometext1"), + stackitem.Make("sometext22"), + stackitem.Make("sometext3"), + }), "getRecords", "neo.com", int64(nns.TXT)) // Delete record. t.Run("invalid witness", func(t *testing.T) { - cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME)) + cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.CNAME)) }) - c.Invoke(t, "nspcc.ru", "getRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME)) - c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.CNAME)) + c.Invoke(t, stackitem.NewArray([]stackitem.Item{ + stackitem.Make("1.2.3.4"), + stackitem.Make("1.2.3.4"), + }), "getRecords", "neo.com", int64(nns.A)) t.Run("SetRecord_compatibility", func(t *testing.T) { // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior @@ -303,9 +328,9 @@ func TestSetGetRecord(t *testing.T) { args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name} t.Run(testCase.Name, func(t *testing.T) { if testCase.ShouldFail { - c.InvokeFail(t, "", "setRecord", args...) + c.InvokeFail(t, "", "addRecord", args...) } else { - c.Invoke(t, stackitem.Null{}, "setRecord", args...) + c.Invoke(t, stackitem.Null{}, "addRecord", args...) } }) } @@ -343,15 +368,15 @@ func TestSetAdmin(t *testing.T) { c.Invoke(t, props, "properties", "neo.com") t.Run("set and delete by admin", func(t *testing.T) { - cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") - cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) - cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") + cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT)) + cAdmin.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.TXT)) }) t.Run("set admin to null", func(t *testing.T) { - cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") + cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) - cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) + cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT)) }) } @@ -367,7 +392,7 @@ func TestTransfer(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash()) - cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") + cFrom.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil) @@ -450,18 +475,27 @@ func TestResolve(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com") cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") - - c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) - c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) - c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT)) - c.Invoke(t, "sometxt", "resolve", "neo.com.", int64(nns.TXT)) - c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.TXT)) - c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) + 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) { @@ -474,14 +508,14 @@ func TestGetAllRecords(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash) cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla0") - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "bla1") // overwrite + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "bla0") + cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 0, "bla1") // overwrite // Add some arbitrary data. cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt") script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com") require.NoError(t, err) @@ -491,21 +525,63 @@ func TestGetAllRecords(t *testing.T) { stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.A), stackitem.NewByteArray([]byte("1.2.3.4")), + stackitem.NewBigInteger(big.NewInt(0)), }), stackitem.NewStruct([]stackitem.Item{ stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.CNAME), stackitem.NewByteArray([]byte("alias.com")), + stackitem.NewBigInteger(big.NewInt(0)), }), stackitem.NewStruct([]stackitem.Item{ stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.TXT), stackitem.NewByteArray([]byte("bla1")), + stackitem.NewBigInteger(big.NewInt(0)), }), })) } +func TestGetRecords(t *testing.T) { + c := newNSClient(t) + e := c.Executor + + acc := e.NewAccount(t) + cAcc := c.WithSigners(acc) + cAccCommittee := c.WithSigners(acc, c.Committee) + + c.Invoke(t, true, "register", "com", c.CommitteeHash) + cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com") + + // Add some arbitrary data. + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) + cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt") + + c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) + // Check empty result of `getRecords`. + c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.AAAA)) +} + +func TestNNSAddRecord(t *testing.T) { + c := newNSClient(t) + cAccCommittee := c.WithSigners(c.Committee) + + c.Invoke(t, true, "register", "com", c.CommitteeHash) + cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash) + + for i := 0; i <= maxRecordID+1; i++ { + if i == maxRecordID+1 { + c.InvokeFail(t, "maximum number of records reached", "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i)) + } else { + c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i)) + } + } +} + const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 + maxRecordID = 255 ) diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index f6683bf2fd..7dff03d7bb 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -177,7 +177,7 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) { t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID)) // Block #15: set A record type with priv0 owner via NNS. - nsPriv0Invoker.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 + nsPriv0Invoker.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15 // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1 From 8790602f69d11d88fc1c4e04e5d4d44be60ebbfb Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 9 Sep 2022 19:36:16 +0300 Subject: [PATCH 11/11] nns: ensure records with the same type are not repeated Port https://github.com/nspcc-dev/neofs-contract/pull/170. --- examples/nft-nd-nns/nns.go | 6 +++++- examples/nft-nd-nns/nns_test.go | 13 ++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 74ac349ab7..d9e71799ce 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -359,8 +359,12 @@ func AddRecord(name string, typ RecordType, data string) { tokenID := checkRecord(ctx, name, typ, data) recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) var id byte - records := storage.Find(ctx, recordsPrefix, storage.KeysOnly) + 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 { diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 664b08aafa..6c2d44af89 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -232,11 +232,8 @@ func TestSetAddGetRecord(t *testing.T) { c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A)) - c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record. - c.Invoke(t, stackitem.NewArray([]stackitem.Item{ - stackitem.Make("1.2.3.4"), - stackitem.Make("1.2.3.4"), - }), "getRecords", "neo.com", int64(nns.A)) + c.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") @@ -265,10 +262,7 @@ func TestSetAddGetRecord(t *testing.T) { c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.CNAME)) - c.Invoke(t, stackitem.NewArray([]stackitem.Item{ - stackitem.Make("1.2.3.4"), - stackitem.Make("1.2.3.4"), - }), "getRecords", "neo.com", int64(nns.A)) + 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 @@ -331,6 +325,7 @@ func TestSetAddGetRecord(t *testing.T) { c.InvokeFail(t, "", "addRecord", args...) } else { 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. } }) }