diff --git a/nns/frostfsid.go b/nns/frostfsid.go new file mode 100644 index 0000000..cc8277d --- /dev/null +++ b/nns/frostfsid.go @@ -0,0 +1,40 @@ +package nns + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/native/management" + "github.com/nspcc-dev/neo-go/pkg/interop/native/std" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +const FrostfsIDNNSName = "frostfsid.frostfs" + +const ( + FrostfsIDNNSTLDPermissionKey = "nns-allow-register-tld" + FrostfsIDTLDRegistrationAllowed = "allow" + FrostfsIDTLDRegistrationDisallowed = "disallow" +) + +func checkFrostfsID(ctx storage.Context, addr interop.Hash160) bool { + if len(addr) == 0 { + return false + } + + frostfsIDAddress := getRecordsByType(ctx, []byte(tokenIDFromName(FrostfsIDNNSName)), FrostfsIDNNSName, TXT) + if len(frostfsIDAddress) == 0 { + return false + } + + decodedBytes := std.Base58Decode([]byte(frostfsIDAddress[1])) + + if len(decodedBytes) < 21 || management.GetContract(decodedBytes[1:21]) == nil { + return false + } + + if res := contract.Call(decodedBytes[1:21], "getValueForSubjectKey", contract.ReadOnly, addr, FrostfsIDNNSTLDPermissionKey).(string); res == FrostfsIDTLDRegistrationAllowed { + return true + } + + return false +} diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 7337b83..64a8a13 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -344,7 +344,7 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str tldBytes := storage.Get(ctx, tldKey) if countZone == 1 { - checkCommittee() + checkCommitteeAndFrostfsID(ctx, owner) if tldBytes != nil { panic("TLD already exists") } @@ -381,7 +381,9 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str if !isValid(owner) { panic("invalid owner") } + common.CheckOwnerWitness(owner) + runtime.BurnGas(GetPrice()) var ( tokenKey = getTokenKey([]byte(name)) @@ -924,6 +926,14 @@ func isValid(address interop.Hash160) bool { return address != nil && len(address) == interop.Hash160Len } +// checkCommitteeAndFrostfsID panics if the script container is not signed by the committee. +// or if the owner does not have permission in FrostfsID. +func checkCommitteeAndFrostfsID(ctx storage.Context, owner interop.Hash160) { + if !checkFrostfsID(ctx, owner) { + checkCommittee() + } +} + // checkCommittee panics if the script container is not signed by the committee. func checkCommittee() { committee := neo.GetCommittee() diff --git a/tests/nns_test.go b/tests/nns_test.go index 49ea4f7..8dc4d19 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -12,10 +12,12 @@ import ( "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/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -23,7 +25,7 @@ const nnsPath = "../nns" const msPerYear = 365 * 24 * time.Hour / time.Millisecond -func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker { +func newNNSInvoker(t *testing.T, addRoot, needFrostfsID bool) (*neotest.ContractInvoker, *neotest.ContractInvoker) { e := newExecutor(t) ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) e.DeployContract(t, ctr, nil) @@ -36,11 +38,27 @@ func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker { "com", c.CommitteeHash, "myemail@frostfs.info", refresh, retry, expire, ttl) } - return c + + if !needFrostfsID { + return c, nil + } + + frostfdID := deployFrostFSIDContract(t, e, e.CommitteeHash) + c.Invoke(t, true, "register", + nns.FrostfsIDNNSName, c.CommitteeHash, + "myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL) + + c.Invoke(t, stackitem.Null{}, "addRecord", + nns.FrostfsIDNNSName, int64(nns.TXT), frostfdID.StringLE()) + + c.Invoke(t, stackitem.Null{}, "addRecord", + nns.FrostfsIDNNSName, int64(nns.TXT), address.Uint160ToString(frostfdID)) + + return c, e.CommitteeInvoker(frostfdID) } func TestNNSGeneric(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) c.Invoke(t, "NNS", "symbol") c.Invoke(t, 0, "decimals") @@ -48,7 +66,7 @@ func TestNNSGeneric(t *testing.T) { } func TestNNSRegisterTLD(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) @@ -91,7 +109,7 @@ func TestNNSRegisterTLD(t *testing.T) { } func TestNNSRegister(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) accTop := c.NewAccount(t) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) @@ -243,7 +261,7 @@ func TestNNSRegister(t *testing.T) { } func TestDeleteDomain(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) acc1 := c.NewAccount(t) c1 := c.WithSigners(c.Committee, acc1) @@ -306,7 +324,7 @@ func TestDeleteDomain(t *testing.T) { } func TestGlobalDomain(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) accTop := c.NewAccount(t) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) @@ -349,7 +367,7 @@ func TestGlobalDomain(t *testing.T) { c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.testdomain.com") } func TestTLDRecord(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) c.Invoke(t, stackitem.Null{}, "addRecord", "com", int64(nns.A), "1.2.3.4") @@ -358,7 +376,7 @@ func TestTLDRecord(t *testing.T) { } func TestNNSRegisterMulti(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) newArgs := func(domain string, account neotest.Signer) []any { return []any{ @@ -407,7 +425,7 @@ func TestNNSRegisterMulti(t *testing.T) { } func TestNNSUpdateSOA(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) c.Invoke(t, true, "register", @@ -429,7 +447,7 @@ func TestNNSUpdateSOA(t *testing.T) { } func TestNNSGetAllRecords(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) c.Invoke(t, true, "register", @@ -469,7 +487,7 @@ func TestNNSGetAllRecords(t *testing.T) { } func TestExpiration(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*10), int64(104) c.Invoke(t, true, "register", @@ -508,7 +526,7 @@ func TestExpiration(t *testing.T) { } func TestNNSSetAdmin(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) c.Invoke(t, true, "register", @@ -527,8 +545,36 @@ func TestNNSSetAdmin(t *testing.T) { "testdomain.com", int64(nns.TXT), "will be added") } +func TestNNS_Frostfsid(t *testing.T) { + nnsInv, f := newNNSInvoker(t, false, true) + + acc, err := wallet.NewAccount() + require.NoError(t, err) + acc.PrivateKey().PublicKey().Bytes() + + nnsUserInv := nnsInv.NewInvoker(nnsInv.Hash, newSigner(t, nnsInv, acc)) + + nnsUserInv.InvokeFail(t, "not witnessed by committee", "register", + "testdomain.com", acc.PublicKey().GetScriptHash(), + "myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL) + + f.Invoke(t, stackitem.Null{}, createSubjectMethod, "", acc.PrivateKey().PublicKey().Bytes()) + + f.Invoke(t, stackitem.Null{}, setSubjectKVMethod, acc.PublicKey().GetScriptHash(), nns.FrostfsIDNNSTLDPermissionKey, nns.FrostfsIDTLDRegistrationAllowed) + + nnsUserInv.Invoke(t, true, "register", + "testdomain.com", acc.PublicKey().GetScriptHash(), + "myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL) + + f.Invoke(t, stackitem.Null{}, setSubjectKVMethod, acc.PublicKey().GetScriptHash(), nns.FrostfsIDNNSTLDPermissionKey, nns.FrostfsIDTLDRegistrationDisallowed) + + nnsUserInv.InvokeFail(t, "not witnessed by committee", "register", + "testdomain.kz", acc.PublicKey().GetScriptHash(), + "myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL) +} + func TestNNSIsAvailable(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) c.Invoke(t, true, "isAvailable", "com") @@ -575,7 +621,7 @@ func TestNNSIsAvailable(t *testing.T) { } func TestNNSRenew(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) acc := c.NewAccount(t) c1 := c.WithSigners(c.Committee, acc) @@ -600,7 +646,7 @@ func TestNNSRenew(t *testing.T) { } func TestNNSResolve(t *testing.T) { - c := newNNSInvoker(t, true) + c, _ := newNNSInvoker(t, true, false) refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) c.Invoke(t, true, "register", @@ -617,7 +663,7 @@ func TestNNSResolve(t *testing.T) { } func TestNNSAndProxy(t *testing.T) { - c := newNNSInvoker(t, false) + c, _ := newNNSInvoker(t, false, false) proxyHash := deployProxyContract(t, c.Executor) proxySigner := neotest.NewContractSigner(proxyHash, func(*transaction.Transaction) []any { return nil })