diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index f04c5525b..c1df13dca 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 230aa7202..bb7edd9ac 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 f0071cfd3..8cf016f1f 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",