From bd3722041a43fb9ac85c79517e903472a3d27cd7 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 5 Sep 2022 17:30:38 +0300 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] *: 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 zcmbtWc|25Y`=7<`*i&|6EXlr2_8iL~`&O1jmch)#Fv^%pLq#M@ohTwIibB>xib&bB zl(m$tloBc`GVkH(dEV#wJ)ifl-}!uI?sG5K^}WB>{k^VpC!R;;T|$*xs-ZwEWUZ!v zF%CL;f%a`&;rvl7`#1I}-<^HbDe&MO9?1#(UOwfxHYin1l`*H}-(Gv`fTtnyL}FB> za)E#h&1k-{;_}k)s@4*1&xBDjO{|d*Q^7yH=3exX9lFMn-l~oPrKgg_eP8UQ6$4h;YRjr&h?;lcg$Ag6KNXwe^6Y_m?52?67ed3wIJP7!i=5|BDO>a-iMi0RanMjMX z4N7zKtOAg&#v{lnP#CpSACl1&fo^M7vS?}I^dLX23n(pZ=n+mGa@6F9#`P5;A8jGD zu9gnes4ojO8Hhub+FQ_Cnwsz$eJD#y0ZP%Af-H5mqIEaT)i&)GxcRF}b%c;8Eq&Of z9`qCEiQ0*S4ru-1mchXfecOrBo01B2z#t8&yUo8?kd~HYqWqH?jrm~j;0@R!wIn?5CUTuh|^34CBg}*n31_y zkkR~O7$@9)PUxzc5+lTBSdO&=%Yx^p0!WO_h=v@?n1;=;0xKRsgpdUCdVUWjM-s_S zrswz1Y7Q%ikpvV40UkFy3rra05GIInI4sZrh5?EEA+9=T(48G^daorB9e(65=K^^V zM~FRCVkDVfs}MjYDgsByJpfXTkW5O3fo1*)O#0o$sl!Z?4@ZaofqdxTumal#&cWaF zx=4i0h$*~d)2NUz!5)pxNpfx^HV38%y7Qy{pjrauo5@4#_S}qrQzZV4B6Tyokz~?u zIHG`(8cqWa;lW|KZGhkgU%!J?f?psVLx?6u9*Q&|llWat-?jcp_R6o9`6cK0U& zg=<5k02(%jA+W#ge=7}!5O6hptx=mzKX6^0V_)i5dDXm{vjqJwAb2Bxab?aY+X-B^(_Sf|P1a7oxqe;9DH8sIKMhjv>a*`T`JM5GBMYlAnlxTUap#sXAQ z^xty4MnNo^)Pqn}EDY;IaKUu^xxO@=%AqsHfFpma1s5>JRJI9BMzSqo2@#M zrD~_GG7@h5?V^w@GiyvHBr^S8)h~^v>d*zm9f-&01|-i6fgy&^KOD5p z=P>w^p)gZ7Us1r(ktU(1$1nC+MpOv^lXbeHjZdrO`Nw$%oe4@mg3YRu@Thp}KId;@ z_?!x~FO4}qa13F;x=0>Ue=nxxv2x-Ky}Uc@iJy0(Qyo5W^&kKW1x@XXk~U~bi(%vY zIa&9#b?E15j3DZLj#_X{xSdOXhoKGcG`G!^Q^?BFf({^y+u8Q&+U1}oF2a=YrwH%z88&75MRom7lh}1i+xz8=k&m8BOIk4&}i!}GS+}3zIIggg$h$v;Z zexUDxGO7);;yd!tZTX}Uz`=9w;B%W1wvQQ5ysIiBFmYm#r$n~A^Y-A$TWx2_&OdKv z{~~@#?c`)VIV$iy*3!9ms6Y0@nNgRs^S7)`t~6;~w4RwpotG9&QHr+18w22>Z5ko~ zORr7OliQ@ElKw!I&*0Ash)WaZ+S%n5IsV|n<96}6(+GbFVoFE#(DBEsttUoSioe$` z+A8l0#w!Yj0U)=PY}@YQhV5oA))bZj?u3gHdgBMTTLhdVzVezbF>UTs(bCXLFUY3_ zjn=y@uqIjtL!7(#MxPm$W~Li{VXxna4IdvIl1nP&T^pfM(GP&c_h69bP>S<520DNe zD%d0qa8L=s@P~&$1-8_L=(A7^d+uM){OPyPJe22l1Jr5iAG6N|`HKSyfWIP*8@L&y za}J^*9KTFg1wF>f>2~~=X4Op6={KL;%$GFU>b(3hFA34<#8Nj|o7UdXHQ z9NJQB?o@K?;Fo<)kqHWWwU!D~?Vh8$yRWsmjEb@0&@upcJNLj2mtdpF)~Hvwdz__0 z-Y4HQeCjm5C^i2Hdb^j}_R(;1+L^h=rqYM)v0P}5J6K{#>gIGt@0fSmnGt!bbzo!* zB%@4*=sQBKi+`(*7B`x;d+FxMir24Ri$cqphh>kSQ@e)Y6WJqhd^;Yxa3Lfrg^P=N z?abX%g^j!Y)(UIy9KEcgm!wvHk}ECLwGGO2S7l^={BFHHN$bkiD7;ffqTM4~v$$XB zc`9BBN;lpp5;)oFGvOYLNvl=uVphD|!B1dU3@%<|v90zj@*zS}ZQ zUp~IkrX0_TJlFBEUaws>gm-rEy3E}>HIWk6n2W*ZO3%|g65S@JIv)GF5BvHbSoo+} zdro}*Ek0ZQ761~(jgaoTq!s#6{h`&_s_mVc%^68`^ZlN->1`#~Zt$MTXmA6svmGvV zohU!7HkNP6o2t8Dc4$L<2wwPyg3kE}Z*y@` zEF}qbu14f(t@KtWMz?p1zKlg!LFE!#p#V>j{r^*jC^G<@xA-I}!`>dRvYyYEm!fSdU`3N=v9VEBrabMYsx2^TA!=Vgj`@^m2eI3@%vB<(=DAs!i zV^hwT6IiLVYpBKbqPCc7|F#P}0{rPQw)3`h@2VEXZ=?E>uR64O({{0jib-dO5~{Qv zmM?#AEI++2%n_yFTKQ230Qb7;pVG9;+H*;_XozMsmOD6ib%Gxt+gcfj`pxBN1QRFO zcB)}TyNXh3)ZGCMGxu%%B97zCbjF7#_LRBAxq<&b9RSq7IW`d2F&z87tP>PHT!3F~ z7!kSx9#ClcpxiT`g=ChYl18ruyv~cE_P%F(p=f2iPn++`!P?+TYmVOBb<-fRzsb1( zzEEsD^lqQy*JtNLXFpOJ(+%-@O=9kjV~zrt@rfle<4N{tkKGObT04XO(}O(*%UFSm z?Z5H~2+LT-xh)f8y8U8H&~7hP#)-th0n6g!HD|q&mAitwQm;?dEov2?m4$Xqf1BL( zu4drK6syDu&OWa_>9V$=^;ZgPaH9a=HKR95RFP$pU=5UwxPAm~eZn=xXp3Q3J^E~!U9$(fYVqj60r~`l% z4^nrWX4e)YbXmz8fLFF(uyKf(p0uzu7kpe4Zp@lSnV&rW3XS3_Fw7rp)w4^V*iRig zB`-U%;|()^zs6-~Vt)qIvyY8M@0L0W8s2C7zr~4!#c30D$Pqf?)KDaMN}@luG3}Eo zMzs0z}BoGW)?H2>Z9LE)Isl_$EhTXqFSZHLgSWBYV}wE7%HK=of{$9pk$Sdml9aXcIkS zlJdbmC2ZeYbyr0U2P*MYu}3afx3vG$r>C2Hg^iS<Ocjc#wB3xh zxnM_7UQx|MmfMGY%H6K%eEfCwX{7r6wiuky(#x5W2c)9_IC-o=o0|Q zXFeG>CH^X)DS>FTyh`T8kT_qMx+!){k)SINFp1!dgny=jWm zg-|Zdp3zrajOi6?=&C23)s5Jreuv}6bJCuMoZF;F6F=N6FX{DWw%JV^`4=dU)=DAP zauS=9u+g*9>&z28O}p&hUknGppIbSyPCo2S^vq-6C*I4}t_Tw@X4NDhx?H6g%-lV; zi8EIpsODdobn%NeiF(~A)U6cY+OF2!#Wyge=Z079gE?bKb*rfn1^6Eov+uY@?4`07 z^U6vrJcH(yc7WA8&m3PiDkIQ#gj=5)L10MFl$Uw*x{&48?bkSu_7T?Pz65P@wxC-e zF~_SH%2Yde0uRs5-}hfW6c+CCRa!i9O4ixiUb#;MV+$1>v1h20B!7HztU*XsKgxL# z7kFi)E(&qpomODMyYyqf>I=|7WhwAS=hgF?@>QKinX7=)S*8O3 zey68P6u!W^_4Rg3Csa^VE}|pkj!N)k$qdqj?ZOQ7rC&aNp1vfO8@0~cmp1z7oFkoj zZ}FI0oE&?ts2AQ-W39hqqew$Vj>ST$ zIm0&$M!sLU-&Yf;e0r*U%IfEFgoS01cHN~Po17hw2_oTq^y%X8AMQ4EmbhH&@ev}{ z&FFkHDB3c(@a`$C>+xD??o)pwl zL3b{en4Qh*(sYbsB%aSs-*rI)`J-&cvsmyn|j zr*V;~>^IjlOP)H%Zse{hEY059rtt~8zT$e2(t6DJanJQDCXc^Q{PcJbf;Y$A2SC<~ zmf7Xd(<1RE-+)PDQsc70w`v2;2>HkLSLUUUM5qnE|K;yPSFOR&!u&Jm=2LrP^2eA3 zdr~}7<===cy}Z52S&^}X#0>DRtc+B&j>KL2Wgc_&f^wPYR9Zh))^>5RYSBn0#2zw; zvu6~3II6N>S4h=QSGn*0^a}4@ur+P%DV|i?A~-)D$SV6%Y`$TghIi!nSmz^uWu8a1 z@oQ{tQeXGUuEwo=BaS%$cy9|gBfKCzjsti-Y8J+*ZtGf5N}P1r(dKvHa$E7kaVFXW z%qwuUo8U>#&yQWp59JGRe|osJX8%w}n>_xm5d#1v63PysJwA>tQIw74zU~*&E(@)8 z-NiL;TYRc}Tksa6Pd({^TbcDqr%-c&Z)ZEDIOWIrg|3%qPf8v=&^xa}*yKEMLI8=m zt>0o8ZfVQhm+RXQ?9A;D7fyPT;8v+P^I)C2wC)lQ37)cNFo`c2xb*6iTfEhM=Hky7 z3OgkrsXAs^EGW)bj!;6Ln9{EIV)h2@Yes5iW)5NwSSx%=Xwq$|m?k8qAcJrbzP<{- zUd$h)mA=N!8I{IYwEO?e9ZzeI>DDY6EgO1UVQ)tX&>PpJ!4p>L`+m{wu-V=Ky%>q4+g-I)YlfkbkgxLcejbtSwcov$ klFgpnxn5yTeXD?cT3@=#FFmfuWBnypQF@beUA!auKi~`gA^-pY 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/20] 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/20] 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/20] 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. } }) } From ea934b8e30756b13cc86597bc6c33bf8a1416be5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:27:02 +0300 Subject: [PATCH 12/20] nns: support SOA records Port https://github.com/nspcc-dev/neofs-contract/pull/133/commits/7f1ec13ae5f2e6671aee38ee03be7502b992ee22 and https://github.com/nspcc-dev/neofs-contract/pull/133/commits/f4762c1b5643382199fe3795a345ac6ba0cb1727. --- examples/nft-nd-nns/nns.go | 58 ++++++++++- examples/nft-nd-nns/nns_test.go | 158 +++++++++++++++++------------- examples/nft-nd-nns/recordtype.go | 2 + internal/basicchain/basic.go | 10 +- 4 files changed, 153 insertions(+), 75 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index d9e71799ce..948bfa1c47 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -246,7 +246,7 @@ func parentExpired(ctx storage.Context, first int, fragments []string) bool { } // Register registers new domain with the specified owner and name if it's available. -func Register(name string, owner interop.Hash160) bool { +func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool { fragments := splitAndCheck(name, false) if fragments == nil { panic("invalid domain name format") @@ -303,14 +303,62 @@ func Register(name string, owner interop.Hash160) bool { ns := NameState{ Owner: owner, Name: name, - Expiration: runtime.GetTime() + millisecondsInYear, + Expiration: runtime.GetTime() + expire*1000, } putNameStateWithKey(ctx, tokenKey, ns) + putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) updateBalance(ctx, []byte(name), owner, +1) postTransfer(oldOwner, owner, []byte(name), nil) return true } +// UpdateSOA updates soa record. +func UpdateSOA(name, email string, refresh, retry, expire, ttl int) { + if len(name) > maxDomainNameLength { + panic("invalid domain name format") + } + ctx := storage.GetContext() + ns := getNameState(ctx, []byte(name)) + ns.checkAdmin() + putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) +} + +// putSoaRecord stores SOA domain record. +func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expire, ttl int) { + data := name + " " + email + " " + + std.Itoa(runtime.GetTime(), 10) + " " + + std.Itoa(refresh, 10) + " " + + std.Itoa(retry, 10) + " " + + std.Itoa(expire, 10) + " " + + std.Itoa(ttl, 10) + tokenId := []byte(tokenIDFromName(name)) + putRecord(ctx, tokenId, name, SOA, 0, data) +} + +// updateSoaSerial updates serial of the corresponding SOA domain record. +func updateSoaSerial(ctx storage.Context, tokenId []byte) { + recordKey := getRecordKey(tokenId, string(tokenId), SOA, 0) + + recBytes := storage.Get(ctx, recordKey) + if recBytes == nil { + panic("SOA record not found") + } + rec := std.Deserialize(recBytes.([]byte)).(RecordState) + + split := std.StringSplitNonEmpty(rec.Data, " ") + if len(split) != 7 { + panic("corrupted SOA record format") + } + split[2] = std.Itoa(runtime.GetTime(), 10) // update serial + rec.Data = split[0] + " " + split[1] + " " + + split[2] + " " + split[3] + " " + + split[4] + " " + split[5] + " " + + split[6] + + recBytes = std.Serialize(rec) + storage.Put(ctx, recordKey, recBytes) +} + // Renew increases domain expiration date. func Renew(name string) int { if len(name) > maxDomainNameLength { @@ -351,6 +399,7 @@ func SetRecord(name string, typ RecordType, id byte, data string) { panic("unknown record") } putRecord(ctx, tokenID, name, typ, id, data) + updateSoaSerial(ctx, tokenID) } // AddRecord adds new record of the specified type to the provided domain. @@ -374,6 +423,7 @@ func AddRecord(name string, typ RecordType, data string) { panic("multiple CNAME records") } putRecord(ctx, tokenID, name, typ, id, data) + updateSoaSerial(ctx, tokenID) } // checkRecord performs record validness check and returns token ID. @@ -411,6 +461,9 @@ func GetRecords(name string, typ RecordType) []string { // DeleteRecords removes all domain records with the specified type. func DeleteRecords(name string, typ RecordType) { + if typ == SOA { + panic("forbidden to delete SOA record") + } tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext() ns := getNameState(ctx, tokenID) @@ -421,6 +474,7 @@ func DeleteRecords(name string, typ RecordType) { key := iterator.Value(records).(string) storage.Delete(ctx, key) } + updateSoaSerial(ctx, tokenID) } // Resolve resolves given name (not more than three redirects are allowed) to a set diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 6c2d44af89..e1dd7452df 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -17,13 +17,24 @@ import ( "github.com/stretchr/testify/require" ) -func newNSClient(t *testing.T) *neotest.ContractInvoker { +const ( + millisecondsInYear = 365 * 24 * 3600 * 1000 + maxDomainNameFragmentLength = 63 +) + +func newNSClient(t *testing.T, registerComTLD bool) *neotest.ContractInvoker { bc, acc := chain.NewSingle(t) e := neotest.NewExecutor(t, bc, acc, acc) - c := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") - e.DeployContract(t, c, nil) + ctr := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") + e.DeployContract(t, ctr, nil) - return e.CommitteeInvoker(c.Hash) + c := e.CommitteeInvoker(ctr.Hash) + if registerComTLD { + // Set expiration big enough to pass all tests. + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) + c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl) + } + return c } func TestNameService_Price(t *testing.T) { @@ -32,7 +43,7 @@ func TestNameService_Price(t *testing.T) { maxPrice = int64(10000_00000000) ) - c := newNSClient(t) + c := newNSClient(t, false) t.Run("set, not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) @@ -65,7 +76,7 @@ func TestNameService_Price(t *testing.T) { } func TestNonfungible(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) c.Signers = []neotest.Signer{c.NewAccount(t)} c.Invoke(t, "NNS", "symbol") @@ -74,38 +85,39 @@ func TestNonfungible(t *testing.T) { } func TestRegisterTLD(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) t.Run("invalid format", func(t *testing.T) { - c.InvokeFail(t, "invalid domain name format", "register", "", c.CommitteeHash) + c.InvokeFail(t, "invalid domain name format", "register", "", c.CommitteeHash, mail, refresh, retry, expire, ttl) }) 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", "register", "some", c.CommitteeHash) + c.InvokeFail(t, "not witnessed by committee", "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl) }) - c.Invoke(t, true, "register", "some", c.CommitteeHash) + c.Invoke(t, true, "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl) t.Run("already exists", func(t *testing.T) { - c.InvokeFail(t, "TLD already exists", "register", "some", c.CommitteeHash) + c.InvokeFail(t, "TLD already exists", "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl) }) } func TestExpiration(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor bc := e.Chain + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) 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) - cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash()) + cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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()) + tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) b2 := e.NewUnsignedBlock(t, tx) b2.Index = b1.Index + 1 b2.PrevHash = b1.Hash() @@ -117,7 +129,7 @@ func TestExpiration(t *testing.T) { b3 := e.NewUnsignedBlock(t, tx) b3.Index = b2.Index + 1 b3.PrevHash = b2.Hash() - b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1) + b3.Timestamp = b1.Timestamp + (uint64(expire)*1000 + 1) require.NoError(t, bc.AddBlock(e.SignBlock(b3))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) // "first.com" has been expired @@ -145,46 +157,42 @@ func TestExpiration(t *testing.T) { // cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT)) } -const ( - millisecondsInYear = 365 * 24 * 3600 * 1000 - maxDomainNameFragmentLength = 63 -) - func TestRegisterAndRenew(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com") - c.Invoke(t, true, "register", "org", c.CommitteeHash) + c.Invoke(t, true, "register", "org", c.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com") - c.Invoke(t, true, "register", "com", c.CommitteeHash) + c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl) 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) - c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash) - 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) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) 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, "register", maxLenFragment+".com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.Invoke(t, true, "isAvailable", "neo.com") c.Invoke(t, 3, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com - c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) + c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) topBlock := e.TopBlock(t) - expectedExpiration := topBlock.Timestamp + millisecondsInYear - c.Invoke(t, false, "register", "neo.com", e.CommitteeHash) + expectedExpiration := topBlock.Timestamp + uint64(expire*1000) + c.Invoke(t, false, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) 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) + c.InvokeFail(t, "invalid domain name format", "register", "-testdomain.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "testdomain-.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.Invoke(t, true, "register", "test-domain.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) }) props := stackitem.NewMap() @@ -211,17 +219,17 @@ func TestRegisterAndRenew(t *testing.T) { } func TestSetAddGetRecord(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) acc := e.NewAccount(t) cAcc := c.WithSigners(acc) - c.Invoke(t, true, "register", "com", c.CommitteeHash) t.Run("set before register", func(t *testing.T) { c.InvokeFail(t, "token not found", "addRecord", "neo.com", int64(nns.TXT), "sometext") }) - c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) + c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) t.Run("invalid parameters", func(t *testing.T) { 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") @@ -333,8 +341,9 @@ func TestSetAddGetRecord(t *testing.T) { } func TestSetAdmin(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) owner := e.NewAccount(t) cOwner := c.WithSigners(owner) @@ -344,11 +353,9 @@ func TestSetAdmin(t *testing.T) { guest := e.NewAccount(t) cGuest := c.WithSigners(guest) - c.Invoke(t, true, "register", "com", c.CommitteeHash) - - 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 + cOwner.InvokeFail(t, "not witnessed by admin", "register", "neo.com", owner.ScriptHash(), mail, refresh, retry, expire, ttl) // admin is committee + cOwnerCommittee.Invoke(t, true, "register", "neo.com", owner.ScriptHash(), mail, refresh, retry, expire, ttl) + expectedExpiration := e.TopBlock(t).Timestamp + uint64(expire)*1000 cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) // Must be witnessed by both owner and admin. @@ -376,8 +383,9 @@ func TestSetAdmin(t *testing.T) { } func TestTransfer(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) from := e.NewAccount(t) cFrom := c.WithSigners(from) @@ -385,8 +393,7 @@ func TestTransfer(t *testing.T) { to := e.NewAccount(t) cTo := c.WithSigners(to) - c.Invoke(t, true, "register", "com", c.CommitteeHash) - cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash()) + cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash(), mail, refresh, retry, expire, ttl) 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) @@ -415,8 +422,9 @@ func TestTransfer(t *testing.T) { } func TestTokensOf(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, false) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) acc1 := e.NewAccount(t) cAcc1Committee := c.WithSigners(acc1, c.Committee) @@ -424,9 +432,9 @@ func TestTokensOf(t *testing.T) { cAcc2Committee := c.WithSigners(acc2, c.Committee) 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()) + c.Invoke(t, true, "register", tld, c.CommitteeHash, mail, refresh, retry, expire, ttl) + cAcc1Committee.Invoke(t, true, "register", "neo.com", acc1.ScriptHash(), mail, refresh, retry, expire, ttl) + cAcc2Committee.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash(), mail, refresh, retry, expire, ttl) testTokensOf(t, c, tld, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) testTokensOf(t, c, tld, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) @@ -461,23 +469,23 @@ func testTokensOf(t *testing.T, c *neotest.ContractInvoker, tld []byte, result [ } func TestResolve(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) 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()) + cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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()) + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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()) + cAccCommittee.Invoke(t, true, "register", "alias2.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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)) @@ -494,22 +502,23 @@ func TestResolve(t *testing.T) { } func TestGetAllRecords(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) 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()) + cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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 + time := e.TopBlock(t).Timestamp // Add some arbitrary data. - cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt") script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com") @@ -528,6 +537,15 @@ func TestGetAllRecords(t *testing.T) { stackitem.NewByteArray([]byte("alias.com")), stackitem.NewBigInteger(big.NewInt(0)), }), + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.Make(nns.SOA), + stackitem.NewBuffer([]byte("neo.com" + " " + mail + " " + + strconv.Itoa(int(time)) + " " + strconv.Itoa(int(refresh)) + " " + + strconv.Itoa(int(retry)) + " " + strconv.Itoa(int(expire)) + " " + + strconv.Itoa(int(ttl)))), + stackitem.NewBigInteger(big.NewInt(0)), + }), stackitem.NewStruct([]stackitem.Item{ stackitem.NewByteArray([]byte("neo.com")), stackitem.Make(nns.TXT), @@ -538,20 +556,20 @@ func TestGetAllRecords(t *testing.T) { } func TestGetRecords(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) e := c.Executor + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) 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()) + cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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()) + cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl) 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)) @@ -560,11 +578,11 @@ func TestGetRecords(t *testing.T) { } func TestNNSAddRecord(t *testing.T) { - c := newNSClient(t) + c := newNSClient(t, true) cAccCommittee := c.WithSigners(c.Committee) + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) - c.Invoke(t, true, "register", "com", c.CommitteeHash) - cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash) + cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash, mail, refresh, retry, expire, ttl) for i := 0; i <= maxRecordID+1; i++ { if i == maxRecordID+1 { diff --git a/examples/nft-nd-nns/recordtype.go b/examples/nft-nd-nns/recordtype.go index 98035f34ac..52229c7a39 100644 --- a/examples/nft-nd-nns/recordtype.go +++ b/examples/nft-nd-nns/recordtype.go @@ -9,6 +9,8 @@ const ( A RecordType = 1 // CNAME represents canonical name record type. CNAME RecordType = 5 + // SOA represents start of authority record type. + SOA RecordType = 6 // TXT represents text record type. TXT RecordType = 16 ) diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index 7dff03d7bb..c22f782345 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -21,7 +21,10 @@ import ( "github.com/stretchr/testify/require" ) -const neoAmount = 99999000 +const ( + neoAmount = 99999000 + millisecondsInYear = 365 * 24 * 3600 * 1000 +) // Init pushes some predefined set of transactions into the given chain, it needs a path to // the root project directory. @@ -165,11 +168,12 @@ 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, true, "register", "com", nsCommitteeInvoker.CommitteeHash) // block #13 + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) + nsCommitteeInvoker.Invoke(t, true, "register", "com", nsCommitteeInvoker.CommitteeHash, mail, refresh, retry, expire, ttl) // block #13 // Block #14: register `neo.com` via NNS. registerTxH := nsPriv0CommitteeInvoker.Invoke(t, true, "register", - "neo.com", priv0ScriptHash) // block #14 + "neo.com", priv0ScriptHash, mail, refresh, retry, expire, ttl) // block #14 res := e.GetTxExecResult(t, registerTxH) require.Equal(t, 1, len(res.Events)) // transfer tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes() From db9cea5ecca92863571b9bf291e1e2ab666b6cdc Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:27:07 +0300 Subject: [PATCH 13/20] nns: allow arbitrary level domains Port https://github.com/nspcc-dev/neofs-contract/pull/175/commits/d10da892d9914238ae686e83553ce6539a3b5458. --- examples/nft-nd-nns/nns.go | 42 ++++++++++++++++++++----------- examples/nft-nd-nns/nns_test.go | 44 ++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 948bfa1c47..c3b04931bc 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -247,7 +247,7 @@ func parentExpired(ctx storage.Context, first int, fragments []string) bool { // Register registers new domain with the specified owner and name if it's available. func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool { - fragments := splitAndCheck(name, false) + fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } @@ -266,9 +266,9 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, panic("TLD not found") } if parentExpired(ctx, 1, fragments) { - panic("one of the parent domains has expired") + panic("one of the parent domains is not registered") } - parentKey := getTokenKey([]byte(fragments[1])) + parentKey := getTokenKey([]byte(name[len(fragments[0])+1:])) nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...)) ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns.checkAdmin() @@ -331,7 +331,7 @@ func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expir std.Itoa(retry, 10) + " " + std.Itoa(expire, 10) + " " + std.Itoa(ttl, 10) - tokenId := []byte(tokenIDFromName(name)) + tokenId := []byte(tokenIDFromName(ctx, name)) putRecord(ctx, tokenId, name, SOA, 0, data) } @@ -428,7 +428,7 @@ func AddRecord(name string, typ RecordType, data string) { // checkRecord performs record validness check and returns token ID. func checkRecord(ctx storage.Context, name string, typ RecordType, data string) []byte { - tokenID := []byte(tokenIDFromName(name)) + tokenID := []byte(tokenIDFromName(ctx, name)) var ok bool switch typ { case A: @@ -453,8 +453,8 @@ func checkRecord(ctx storage.Context, name string, typ RecordType, data 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() + tokenID := []byte(tokenIDFromName(ctx, name)) _ = getNameState(ctx, tokenID) // ensure not expired return getRecordsByType(ctx, tokenID, name, typ) } @@ -464,8 +464,8 @@ func DeleteRecords(name string, typ RecordType) { if typ == SOA { panic("forbidden to delete SOA record") } - tokenID := []byte(tokenIDFromName(name)) ctx := storage.GetContext() + tokenID := []byte(tokenIDFromName(ctx, name)) ns := getNameState(ctx, tokenID) ns.checkAdmin() recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ) @@ -554,7 +554,7 @@ func getNameState(ctx storage.Context, tokenID []byte) NameState { // getNameStateWithKey returns domain name state by the specified token key. func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { - nameKey := append([]byte{prefixName}, tokenKey...) + nameKey := getNameStateKey(tokenKey) nsBytes := storage.Get(ctx, nameKey) if nsBytes == nil { panic("token not found") @@ -564,6 +564,11 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { return ns } +// getNameStateKey returns NameState key for the provided token key. +func getNameStateKey(tokenKey []byte) []byte { + return append([]byte{prefixName}, tokenKey...) +} + // putNameState stores domain name state. func putNameState(ctx storage.Context, ns NameState) { tokenKey := getTokenKey([]byte(ns.Name)) @@ -806,16 +811,25 @@ func checkIPv6(data string) bool { } // tokenIDFromName returns token ID (domain.root) from provided name. -func tokenIDFromName(name string) string { +func tokenIDFromName(ctx storage.Context, name string) string { fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } - l := len(fragments) - if l == 1 { - return name + sum := 0 + for i := 0; i < len(fragments)-1; i++ { + tokenKey := getTokenKey([]byte(name[sum:])) + nameKey := getNameStateKey(tokenKey) + nsBytes := storage.Get(ctx, nameKey) + if nsBytes != nil { + ns := std.Deserialize(nsBytes.([]byte)).(NameState) + if runtime.GetTime() < ns.Expiration { + return name[sum:] + } + } + sum += len(fragments[i]) + 1 } - return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):] + return name } // resolve resolves provided name using record with the specified type and given @@ -855,7 +869,7 @@ func resolve(ctx storage.Context, res []string, name string, typ RecordType, red // 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)) + tokenID := []byte(tokenIDFromName(ctx, name)) _ = 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_test.go b/examples/nft-nd-nns/nns_test.go index e1dd7452df..1441baa2b8 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -168,7 +168,7 @@ func TestRegisterAndRenew(t *testing.T) { c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl) c.Invoke(t, true, "isAvailable", "neo.com") c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) @@ -593,6 +593,48 @@ func TestNNSAddRecord(t *testing.T) { } } +func TestNNSRegisterArbitraryLevelDomain(t *testing.T) { + c := newNSClient(t, true) + + newArgs := func(domain string, account neotest.Signer) []interface{} { + return []interface{}{ + domain, account.ScriptHash(), "doesnt@matter.com", + int64(101), int64(102), int64(103), int64(104), + } + } + acc := c.NewAccount(t) + cBoth := c.WithSigners(c.Committee, acc) + args := newArgs("neo.com", acc) + cBoth.Invoke(t, true, "register", args...) + + c1 := c.WithSigners(acc) + // parent domain is missing + args[0] = "testnet.fs.neo.com" + c1.InvokeFail(t, "one of the parent domains is not registered", "register", args...) + + args[0] = "fs.neo.com" + c1.Invoke(t, true, "register", args...) + + args[0] = "testnet.fs.neo.com" + c1.Invoke(t, true, "register", args...) + + acc2 := c.NewAccount(t) + c2 := c.WithSigners(c.Committee, acc2) + args = newArgs("mainnet.fs.neo.com", acc2) + c2.InvokeFail(t, "not witnessed by admin", "register", args...) + + c2 = c.WithSigners(acc, acc2) + c2.Invoke(t, true, "register", args...) + + c2 = c.WithSigners(acc2) + c2.Invoke(t, stackitem.Null{}, "addRecord", + "cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13") + result := stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("166.15.14.13")), + }) + c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A)) +} + const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 From d5b1c0e429679432c35109476d5d77f041f427a5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:27:09 +0300 Subject: [PATCH 14/20] nns: check for conflicting records on domain registration Port https://github.com/nspcc-dev/neofs-contract/pull/175/commits/f25296b17a4dcaca50855c40d44a42bbcf0bb6a1. --- examples/nft-nd-nns/nns.go | 11 +++++++++++ examples/nft-nd-nns/nns_test.go | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index c3b04931bc..f3e09d9508 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -272,6 +272,17 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...)) ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns.checkAdmin() + + parentRecKey := append([]byte{prefixRecord}, parentKey...) + it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues) + suffix := []byte(name) + for iterator.Next(it) { + r := iterator.Value(it).(RecordState) + ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name)) + if ind > 0 && ind+len(suffix) == len(r.Name) { + panic("parent domain has conflicting records: " + r.Name) + } + } } if !isValid(owner) { diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 1441baa2b8..09e96a46f2 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -623,7 +623,17 @@ func TestNNSRegisterArbitraryLevelDomain(t *testing.T) { args = newArgs("mainnet.fs.neo.com", acc2) c2.InvokeFail(t, "not witnessed by admin", "register", args...) + c1.Invoke(t, stackitem.Null{}, "addRecord", + "something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4") + c1.Invoke(t, stackitem.Null{}, "addRecord", + "another.fs.neo.com", int64(nns.A), "4.3.2.1") + c2 = c.WithSigners(acc, acc2) + c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com", + "register", args...) + + c1.Invoke(t, stackitem.Null{}, "deleteRecords", + "something.mainnet.fs.neo.com", int64(nns.A)) c2.Invoke(t, true, "register", args...) c2 = c.WithSigners(acc2) From d8e0a02a865dde1f16315f719c9181fc7b4e69eb Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:27:10 +0300 Subject: [PATCH 15/20] nns: keep `isAvailable` in sync with `register` If conflicting records '*.domain' are present on new domain registration, then `isAvailable` should return false for this domain. Ref. https://github.com/nspcc-dev/neofs-contract/pull/175/commits/f25296b17a4dcaca50855c40d44a42bbcf0bb6a1. --- examples/nft-nd-nns/nns.go | 35 ++++++++++++++++++++++----------- examples/nft-nd-nns/nns_test.go | 28 ++++++++------------------ 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index f3e09d9508..bc5ad890c3 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -208,7 +208,7 @@ func GetPrice() int { // IsAvailable checks whether provided domain name is available. func IsAvailable(name string) bool { - fragments := splitAndCheck(name, false) + fragments := splitAndCheck(name, true) if fragments == nil { panic("invalid domain name format") } @@ -220,7 +220,27 @@ func IsAvailable(name string) bool { } return true } - return parentExpired(ctx, 0, fragments) + if !parentExpired(ctx, 0, fragments) { + return false + } + return len(getParentConflictingRecord(ctx, name, fragments)) == 0 +} + +// getPrentConflictingRecord returns record of '*.name' format if they are presented. +// These records conflict with domain name to be registered. +func getParentConflictingRecord(ctx storage.Context, name string, fragments []string) string { + parentKey := getTokenKey([]byte(name[len(fragments[0])+1:])) + parentRecKey := append([]byte{prefixRecord}, parentKey...) + it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues) + suffix := []byte(name) + for iterator.Next(it) { + r := iterator.Value(it).(RecordState) + ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name)) + if ind > 0 && ind+len(suffix) == len(r.Name) { + return r.Name + } + } + return "" } // parentExpired returns true if any domain from fragments doesn't exist or expired. @@ -273,15 +293,8 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns.checkAdmin() - parentRecKey := append([]byte{prefixRecord}, parentKey...) - it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues) - suffix := []byte(name) - for iterator.Next(it) { - r := iterator.Value(it).(RecordState) - ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name)) - if ind > 0 && ind+len(suffix) == len(r.Name) { - panic("parent domain has conflicting records: " + r.Name) - } + if conflict := getParentConflictingRecord(ctx, name, fragments); len(conflict) != 0 { + panic("parent domain has conflicting records: " + conflict) } } diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 09e96a46f2..b43b1e32c9 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -125,29 +125,15 @@ func TestExpiration(t *testing.T) { require.NoError(t, bc.AddBlock(e.SignBlock(b2))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) - tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com") - b3 := e.NewUnsignedBlock(t, tx) + b3 := e.NewUnsignedBlock(t) b3.Index = b2.Index + 1 b3.PrevHash = b2.Hash() - b3.Timestamp = b1.Timestamp + (uint64(expire)*1000 + 1) + b3.Timestamp = b1.Timestamp + (uint64(expire) * 1000) require.NoError(t, bc.AddBlock(e.SignBlock(b3))) - 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) - b4.Index = b3.Index + 1 - b4.PrevHash = b3.Hash() - b4.Timestamp = b3.Timestamp + 1000 - 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, "getRecords", "first.com", int64(nns.TXT)) - b5 := e.NewUnsignedBlock(t, tx) - b5.Index = b4.Index + 1 - b5.PrevHash = b4.Hash() - b5.Timestamp = b4.Timestamp + 1000 - require.NoError(t, bc.AddBlock(e.SignBlock(b5))) - e.CheckFault(t, tx.Hash(), "name has expired") + + cAcc.Invoke(t, true, "isAvailable", "first.com") // "first.com" has been expired + cAcc.Invoke(t, true, "isAvailable", "second.com") // TLD "com" has been expired + cAcc.InvokeFail(t, "name has expired", "getRecords", "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 @@ -629,11 +615,13 @@ func TestNNSRegisterArbitraryLevelDomain(t *testing.T) { "another.fs.neo.com", int64(nns.A), "4.3.2.1") c2 = c.WithSigners(acc, acc2) + c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.fs.neo.com") c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com", "register", args...) c1.Invoke(t, stackitem.Null{}, "deleteRecords", "something.mainnet.fs.neo.com", int64(nns.A)) + c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.fs.neo.com") c2.Invoke(t, true, "register", args...) c2 = c.WithSigners(acc2) From 14c9858df287b0040f35046ed81033697936519e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:27:12 +0300 Subject: [PATCH 16/20] nns: use MillisecondsInSecond constant where appropriate --- examples/nft-nd-nns/nns.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index bc5ad890c3..6f93490c4e 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -64,8 +64,10 @@ const ( const ( // defaultRegisterPrice is the default price for new domain registration. defaultRegisterPrice = 10_0000_0000 + // millisecondsInSecond is the amount of milliseconds per second. + millisecondsInSecond = 1000 // millisecondsInYear is amount of milliseconds per year. - millisecondsInYear = 365 * 24 * 3600 * 1000 + millisecondsInYear = 365 * 24 * 3600 * millisecondsInSecond ) // RecordState is a type that registered entities are saved to. @@ -327,7 +329,7 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, ns := NameState{ Owner: owner, Name: name, - Expiration: runtime.GetTime() + expire*1000, + Expiration: runtime.GetTime() + expire*millisecondsInSecond, } putNameStateWithKey(ctx, tokenKey, ns) putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) From dea82ac4dde5f57ac31b974a1398f32a58b9affb Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Sep 2022 22:42:39 +0300 Subject: [PATCH 17/20] nns: make domain registration price dependant on length Port https://github.com/neo-project/non-native-contracts/commit/8ac2b807343e2e88a3a6534ed15594216db7a72b. --- examples/nft-nd-nns/nns.go | 60 +++++++++++++++---- examples/nft-nd-nns/nns_test.go | 101 ++++++++++++++++++-------------- 2 files changed, 106 insertions(+), 55 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 6f93490c4e..c4a8f40ee4 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -29,7 +29,8 @@ const ( // prefixAccountToken contains map from (owner + token key) to token ID, // where token key = hash160(token ID) and token ID = domain name. prefixAccountToken byte = 0x02 - // prefixRegisterPrice contains price for new domain name registration. + // prefixRegisterPrice contains list of prices for new domain name registration + // depending on the domain name length. prefixRegisterPrice byte = 0x10 // prefixRoot contains set of roots (map from root to 0). prefixRoot byte = 0x20 @@ -91,7 +92,13 @@ func _deploy(data interface{}, isUpdate bool) { } ctx := storage.GetContext() storage.Put(ctx, []byte{prefixTotalSupply}, 0) - storage.Put(ctx, []byte{prefixRegisterPrice}, defaultRegisterPrice) + storage.Put(ctx, []byte{prefixRegisterPrice}, std.Serialize([]int{ + defaultRegisterPrice, // Prices for all other lengths of domain names. + -1, // Domain names with a length of 1 are not open for registration by default. + -1, // Domain names with a length of 2 are not open for registration by default. + -1, // Domain names with a length of 3 are not open for registration by default. + -1, // Domain names with a length of 4 are not open for registration by default. + })) } // Symbol returns NeoNameService symbol. @@ -193,19 +200,31 @@ func Roots() iterator.Iterator { } // SetPrice sets the domain registration price. -func SetPrice(price int) { +func SetPrice(priceList []int) { checkCommittee() - if price < 0 || price > maxRegisterPrice { - panic("The price is out of range.") + if len(priceList) == 0 { + panic("price list is empty") + } + for i := 0; i < len(priceList); i++ { + if i == 0 && priceList[i] == -1 { + panic("default price is out of range") + } + if priceList[i] < -1 || priceList[i] > maxRegisterPrice { + panic("price is out of range") + } } ctx := storage.GetContext() - storage.Put(ctx, []byte{prefixRegisterPrice}, price) + storage.Put(ctx, []byte{prefixRegisterPrice}, std.Serialize(priceList)) } -// GetPrice returns the domain registration price. -func GetPrice() int { +// GetPrice returns the domain registration price depending on the domain name length. +func GetPrice(length int) int { ctx := storage.GetReadOnlyContext() - return storage.Get(ctx, []byte{prefixRegisterPrice}).(int) + priceList := std.Deserialize(storage.Get(ctx, []byte{prefixRegisterPrice}).([]byte)).([]int) + if length >= len(priceList) { + length = 0 + } + return priceList[length] } // IsAvailable checks whether provided domain name is available. @@ -222,6 +241,9 @@ func IsAvailable(name string) bool { } return true } + if GetPrice(len(fragments[0])) < 0 { + return false + } if !parentExpired(ctx, 0, fragments) { return false } @@ -306,7 +328,13 @@ func Register(name string, owner interop.Hash160, email string, refresh, retry, if !runtime.CheckWitness(owner) { panic("not witnessed by owner") } - runtime.BurnGas(GetPrice()) + price := GetPrice(len(fragments[0])) + if price < 0 { + checkCommittee() + } else { + runtime.BurnGas(price) + } + var ( tokenKey = getTokenKey([]byte(name)) oldOwner interop.Hash160 @@ -390,7 +418,17 @@ func Renew(name string) int { if len(name) > maxDomainNameLength { panic("invalid domain name format") } - runtime.BurnGas(GetPrice()) + fragments := splitAndCheck(name, true) + if fragments == nil { + panic("invalid domain name format") + } + price := GetPrice(len(fragments[0])) + if price < 0 { + checkCommittee() + } else { + runtime.BurnGas(price) + } + ctx := storage.GetContext() ns := getNameState(ctx, []byte(name)) ns.Expiration += millisecondsInYear diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index b43b1e32c9..4b2d10534e 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -39,8 +39,10 @@ func newNSClient(t *testing.T, registerComTLD bool) *neotest.ContractInvoker { func TestNameService_Price(t *testing.T) { const ( - minPrice = int64(0) - maxPrice = int64(10000_00000000) + minPrice = int64(-1) + maxPrice = int64(10000_00000000) + defaultPrice = 10_0000_0000 + committeePrice = -1 ) c := newNSClient(t, false) @@ -50,28 +52,38 @@ func TestNameService_Price(t *testing.T) { cAcc := c.WithSigners(acc) cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1) }) - t.Run("get, default value", func(t *testing.T) { - c.Invoke(t, defaultNameServiceDomainPrice, "getPrice") + c.Invoke(t, defaultPrice, "getPrice", 0) + c.Invoke(t, committeePrice, "getPrice", 1) + c.Invoke(t, committeePrice, "getPrice", 2) + c.Invoke(t, committeePrice, "getPrice", 3) + c.Invoke(t, committeePrice, "getPrice", 4) + c.Invoke(t, defaultPrice, "getPrice", 5) }) - t.Run("set, too small value", func(t *testing.T) { - c.InvokeFail(t, "The price is out of range.", "setPrice", minPrice-1) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{minPrice - 1}) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{defaultPrice, minPrice - 1}) }) - t.Run("set, too large value", func(t *testing.T) { - c.InvokeFail(t, "The price is out of range.", "setPrice", maxPrice+1) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{minPrice - 1}) + c.InvokeFail(t, "price is out of range", "setPrice", []interface{}{defaultPrice, minPrice - 1}) + }) + t.Run("set, negative default price", func(t *testing.T) { + c.InvokeFail(t, "default price is out of range", "setPrice", []interface{}{committeePrice, minPrice + 1}) }) - t.Run("set, success", func(t *testing.T) { - txSet := c.PrepareInvoke(t, "setPrice", int64(defaultNameServiceDomainPrice+1)) - txGet := c.PrepareInvoke(t, "getPrice") - c.AddBlockCheckHalt(t, txSet, txGet) + txSet := c.PrepareInvoke(t, "setPrice", []interface{}{defaultPrice - 1, committeePrice, committeePrice, committeePrice, committeePrice, committeePrice}) + txGet1 := c.PrepareInvoke(t, "getPrice", 5) + txGet2 := c.PrepareInvoke(t, "getPrice", 6) + c.AddBlockCheckHalt(t, txSet, txGet1, txGet2) c.CheckHalt(t, txSet.Hash(), stackitem.Null{}) - c.CheckHalt(t, txGet.Hash(), stackitem.Make(defaultNameServiceDomainPrice+1)) + c.CheckHalt(t, txGet1.Hash(), stackitem.Make(committeePrice)) + c.CheckHalt(t, txGet2.Hash(), stackitem.Make(defaultPrice-1)) // Get in the next block. - c.Invoke(t, stackitem.Make(defaultNameServiceDomainPrice+1), "getPrice") + c.Invoke(t, stackitem.Make(committeePrice), "getPrice", 2) + c.Invoke(t, stackitem.Make(committeePrice), "getPrice", 5) + c.Invoke(t, stackitem.Make(defaultPrice-1), "getPrice", 6) }) } @@ -148,17 +160,17 @@ func TestRegisterAndRenew(t *testing.T) { e := c.Executor mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) - c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com") + c.InvokeFail(t, "TLD not found", "isAvailable", "neo-go.com") c.Invoke(t, true, "register", "org", c.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com") + c.InvokeFail(t, "TLD not found", "isAvailable", "neo-go.com") c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl) - c.Invoke(t, true, "isAvailable", "neo.com") - c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.Invoke(t, true, "isAvailable", "neo-go.com") + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo-go.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo-go.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "\nneo-go.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeFail(t, "invalid domain name format", "register", "neo-go.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo-go.org", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo-go.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) var maxLenFragment string for i := 0; i < maxDomainNameFragmentLength; i++ { maxLenFragment += "q" @@ -167,13 +179,13 @@ func TestRegisterAndRenew(t *testing.T) { c.Invoke(t, true, "register", maxLenFragment+".com", e.CommitteeHash, mail, refresh, retry, expire, ttl) c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.Invoke(t, true, "isAvailable", "neo.com") + c.Invoke(t, true, "isAvailable", "neo-go.com") c.Invoke(t, 3, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com - c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.Invoke(t, true, "register", "neo-go.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) topBlock := e.TopBlock(t) expectedExpiration := topBlock.Timestamp + uint64(expire*1000) - c.Invoke(t, false, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) - c.Invoke(t, false, "isAvailable", "neo.com") + c.Invoke(t, false, "register", "neo-go.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) + c.Invoke(t, false, "isAvailable", "neo-go.com") t.Run("domain names with hyphen", func(t *testing.T) { c.InvokeFail(t, "invalid domain name format", "register", "-testdomain.com", e.CommitteeHash, mail, refresh, retry, expire, ttl) @@ -182,12 +194,12 @@ func TestRegisterAndRenew(t *testing.T) { }) props := stackitem.NewMap() - props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) + props.Add(stackitem.Make("name"), stackitem.Make("neo-go.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, props, "properties", "neo-go.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")) + c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo-go.com")) t.Run("invalid token ID", func(t *testing.T) { c.InvokeFail(t, "token not found", "properties", "not.exists") @@ -198,10 +210,10 @@ func TestRegisterAndRenew(t *testing.T) { // Renew expectedExpiration += millisecondsInYear - c.Invoke(t, expectedExpiration, "renew", "neo.com") + c.Invoke(t, expectedExpiration, "renew", "neo-go.com") props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) - c.Invoke(t, props, "properties", "neo.com") + c.Invoke(t, props, "properties", "neo-go.com") } func TestSetAddGetRecord(t *testing.T) { @@ -594,43 +606,44 @@ func TestNNSRegisterArbitraryLevelDomain(t *testing.T) { cBoth.Invoke(t, true, "register", args...) c1 := c.WithSigners(acc) + // Use long (>4 chars) domain name to avoid committee signature check. // parent domain is missing - args[0] = "testnet.fs.neo.com" + args[0] = "testnet.filestorage.neo.com" c1.InvokeFail(t, "one of the parent domains is not registered", "register", args...) - args[0] = "fs.neo.com" + args[0] = "filestorage.neo.com" c1.Invoke(t, true, "register", args...) - args[0] = "testnet.fs.neo.com" + args[0] = "testnet.filestorage.neo.com" c1.Invoke(t, true, "register", args...) acc2 := c.NewAccount(t) c2 := c.WithSigners(c.Committee, acc2) - args = newArgs("mainnet.fs.neo.com", acc2) + args = newArgs("mainnet.filestorage.neo.com", acc2) c2.InvokeFail(t, "not witnessed by admin", "register", args...) c1.Invoke(t, stackitem.Null{}, "addRecord", - "something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4") + "something.mainnet.filestorage.neo.com", int64(nns.A), "1.2.3.4") c1.Invoke(t, stackitem.Null{}, "addRecord", - "another.fs.neo.com", int64(nns.A), "4.3.2.1") + "another.filestorage.neo.com", int64(nns.A), "4.3.2.1") c2 = c.WithSigners(acc, acc2) - c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.fs.neo.com") - c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com", + c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.filestorage.neo.com") + c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.filestorage.neo.com", "register", args...) c1.Invoke(t, stackitem.Null{}, "deleteRecords", - "something.mainnet.fs.neo.com", int64(nns.A)) - c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.fs.neo.com") + "something.mainnet.filestorage.neo.com", int64(nns.A)) + c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.filestorage.neo.com") c2.Invoke(t, true, "register", args...) c2 = c.WithSigners(acc2) c2.Invoke(t, stackitem.Null{}, "addRecord", - "cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13") + "cdn.mainnet.filestorage.neo.com", int64(nns.A), "166.15.14.13") result := stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray([]byte("166.15.14.13")), }) - c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A)) + c2.Invoke(t, result, "resolve", "cdn.mainnet.filestorage.neo.com", int64(nns.A)) } const ( From 9eed7c856e0e4ff21fd46d30bfca13d52a83c9d3 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 16 Sep 2022 11:31:53 +0300 Subject: [PATCH 18/20] nns: accept expiration period in `renew` Port https://github.com/neo-project/non-native-contracts/pull/13. --- examples/nft-nd-nns/nns.go | 21 +++++++++++++++++---- examples/nft-nd-nns/nns.yml | 2 ++ examples/nft-nd-nns/nns_test.go | 11 ++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index c4a8f40ee4..e83454d494 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -69,6 +69,8 @@ const ( millisecondsInSecond = 1000 // millisecondsInYear is amount of milliseconds per year. millisecondsInYear = 365 * 24 * 3600 * millisecondsInSecond + // millisecondsInTenYears is the amount of milliseconds per ten years. + millisecondsInTenYears = 10 * millisecondsInYear ) // RecordState is a type that registered entities are saved to. @@ -413,8 +415,16 @@ func updateSoaSerial(ctx storage.Context, tokenId []byte) { storage.Put(ctx, recordKey, recBytes) } -// Renew increases domain expiration date. -func Renew(name string) int { +// RenewDefault increases domain expiration date up to 1 year. +func RenewDefault(name string) int { + return Renew(name, 1) +} + +// Renew increases domain expiration date up to the specified amount of years. +func Renew(name string, years int64) int { + if years < 1 || years > 10 { + panic("invalid renewal period value") + } if len(name) > maxDomainNameLength { panic("invalid domain name format") } @@ -426,12 +436,15 @@ func Renew(name string) int { if price < 0 { checkCommittee() } else { - runtime.BurnGas(price) + runtime.BurnGas(price * int(years)) } ctx := storage.GetContext() ns := getNameState(ctx, []byte(name)) - ns.Expiration += millisecondsInYear + ns.Expiration += int(millisecondsInYear * years) + if ns.Expiration > int(runtime.GetTime())+millisecondsInTenYears { + panic("10 years of expiration period at max is allowed") + } putNameState(ctx, ns) return ns.Expiration } diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 4c25081729..2dc880ca2d 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -19,3 +19,5 @@ permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd methods: ["update"] - methods: ["onNEP11Payment"] +overloads: + renewDefault: renew diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index 4b2d10534e..d03ce9c30c 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -158,7 +158,7 @@ func TestExpiration(t *testing.T) { func TestRegisterAndRenew(t *testing.T) { c := newNSClient(t, false) e := c.Executor - mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104) + mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*2), int64(104) c.InvokeFail(t, "TLD not found", "isAvailable", "neo-go.com") c.Invoke(t, true, "register", "org", c.CommitteeHash, mail, refresh, retry, expire, ttl) @@ -214,6 +214,15 @@ func TestRegisterAndRenew(t *testing.T) { props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) c.Invoke(t, props, "properties", "neo-go.com") + + // Invalid renewal period. + c.InvokeFail(t, "invalid renewal period value", "renew", "neo-go.com", 11) + // Too large expiration period. + c.InvokeFail(t, "10 years of expiration period at max is allowed", "renew", "neo-go.com", 10) + + // Non-default renewal period. + mult := 2 + c.Invoke(t, expectedExpiration+uint64(mult*millisecondsInYear), "renew", "neo-go.com", mult) } func TestSetAddGetRecord(t *testing.T) { From 748a7d3eeac80c80f00652ccd55c036fd7e4fcc1 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 16 Sep 2022 11:37:33 +0300 Subject: [PATCH 19/20] nns: add SetAdmin event Port https://github.com/neo-project/non-native-contracts/pull/11. --- examples/nft-nd-nns/nns.go | 2 ++ examples/nft-nd-nns/nns.yml | 8 ++++++++ examples/nft-nd-nns/nns_test.go | 24 ++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index e83454d494..8aa2d8de77 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -462,8 +462,10 @@ func SetAdmin(name string, admin interop.Hash160) { if !runtime.CheckWitness(ns.Owner) { panic("not witnessed by owner") } + oldAdm := ns.Admin ns.Admin = admin putNameState(ctx, ns) + runtime.Notify("SetAdmin", name, oldAdm, admin) } // SetRecord updates record of the specified type and ID. diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 2dc880ca2d..a992344370 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -15,6 +15,14 @@ events: type: Integer - name: tokenId type: ByteArray + - name: SetAdmin + parameters: + - name: name + type: String + - name: oldAdmin + type: Hash160 + - name: newAdmin + type: Hash160 permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd methods: ["update"] diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index d03ce9c30c..ac79c13102 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -9,6 +9,7 @@ import ( nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/core/state" "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" @@ -369,7 +370,17 @@ func TestSetAdmin(t *testing.T) { cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.ScriptHash()) 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()) + h := cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) + cc.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: cc.Hash, + Name: "SetAdmin", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.Null{}, + stackitem.NewByteArray(admin.ScriptHash().BytesBE()), + }), + }) + props := stackitem.NewMap() props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) @@ -384,7 +395,16 @@ func TestSetAdmin(t *testing.T) { t.Run("set admin to null", func(t *testing.T) { cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext") - cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) + h = cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) + cc.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: cc.Hash, + Name: "SetAdmin", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo.com")), + stackitem.NewByteArray(admin.ScriptHash().BytesBE()), + stackitem.Null{}, + }), + }) cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT)) }) } From abd332f7646ef84a9cbaffbdae1012aa584cc1d0 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 16 Sep 2022 11:43:48 +0300 Subject: [PATCH 20/20] nns: add Renew event Port https://github.com/neo-project/non-native-contracts/pull/31. --- examples/nft-nd-nns/nns.go | 2 ++ examples/nft-nd-nns/nns.yml | 8 ++++++++ examples/nft-nd-nns/nns_test.go | 25 +++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 8aa2d8de77..aeff8dcdfc 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -441,11 +441,13 @@ func Renew(name string, years int64) int { ctx := storage.GetContext() ns := getNameState(ctx, []byte(name)) + oldExpiration := ns.Expiration ns.Expiration += int(millisecondsInYear * years) if ns.Expiration > int(runtime.GetTime())+millisecondsInTenYears { panic("10 years of expiration period at max is allowed") } putNameState(ctx, ns) + runtime.Notify("Renew", name, oldExpiration, ns.Expiration) return ns.Expiration } diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index a992344370..4d09e6d542 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -23,6 +23,14 @@ events: type: Hash160 - name: newAdmin type: Hash160 + - name: Renew + parameters: + - name: name + type: String + - name: oldExpiration + type: Integer + - name: newExpiration + type: Integer permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd methods: ["update"] diff --git a/examples/nft-nd-nns/nns_test.go b/examples/nft-nd-nns/nns_test.go index ac79c13102..b294885ba1 100644 --- a/examples/nft-nd-nns/nns_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -210,8 +210,18 @@ func TestRegisterAndRenew(t *testing.T) { }) // Renew + oldExpiration := expectedExpiration expectedExpiration += millisecondsInYear - c.Invoke(t, expectedExpiration, "renew", "neo-go.com") + h := c.Invoke(t, expectedExpiration, "renew", "neo-go.com") + c.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "Renew", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo-go.com")), + stackitem.Make(oldExpiration), + stackitem.Make(expectedExpiration), + }), + }) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) c.Invoke(t, props, "properties", "neo-go.com") @@ -222,8 +232,19 @@ func TestRegisterAndRenew(t *testing.T) { c.InvokeFail(t, "10 years of expiration period at max is allowed", "renew", "neo-go.com", 10) // Non-default renewal period. + oldExpiration = expectedExpiration mult := 2 - c.Invoke(t, expectedExpiration+uint64(mult*millisecondsInYear), "renew", "neo-go.com", mult) + expectedExpiration += uint64(mult * millisecondsInYear) + h = c.Invoke(t, expectedExpiration, "renew", "neo-go.com", mult) + c.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: c.Hash, + Name: "Renew", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("neo-go.com")), + stackitem.Make(oldExpiration), + stackitem.Make(expectedExpiration), + }), + }) } func TestSetAddGetRecord(t *testing.T) {