diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f9b995..5c053d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ Changelog for FrostFS Contract ### Added - Field `state` to a namespace to indicate its' lifecycle stage (#154). - Method `UpdateNamespace` to adjust namespace state (#154). -- Method `DeleteNamespace` to remove existing namespace (#168). ### Changed ### Removed diff --git a/VERSION b/VERSION index e124b61..f198b15 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.21.4 +v0.21.2 diff --git a/common/address.go b/common/address.go deleted file mode 100644 index a5a713a..0000000 --- a/common/address.go +++ /dev/null @@ -1,10 +0,0 @@ -package common - -import "github.com/nspcc-dev/neo-go/pkg/interop" - -const ( - NEO3PrefixLen = 1 - ChecksumLen = 4 - - AddressLen = NEO3PrefixLen + interop.Hash160Len + ChecksumLen -) diff --git a/common/version.go b/common/version.go index b72ea06..81854c3 100644 --- a/common/version.go +++ b/common/version.go @@ -5,7 +5,7 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/native/std" const ( major = 0 minor = 21 - patch = 4 + patch = 2 // Versions from which an update should be performed. // These should be used in a group (so prevMinor can be equal to minor if there are diff --git a/frostfsid/client/client.go b/frostfsid/client/client.go index 8f95b00..57f5298 100644 --- a/frostfsid/client/client.go +++ b/frostfsid/client/client.go @@ -114,7 +114,6 @@ const ( createNamespaceMethod = "createNamespace" updateNamespaceMethod = "updateNamespace" - deleteNamespaceMethod = "deleteNamespace" getNamespaceMethod = "getNamespace" getNamespaceExtendedMethod = "getNamespaceExtended" listNamespacesMethod = "listNamespaces" @@ -464,18 +463,6 @@ func (c Client) UpdateNamespaceCall(namespace string, state string) (method stri return updateNamespaceMethod, []any{namespace, state} } -// DeleteNamespace idempotently removes the namespace. -// Must be invoked by contract owner. -func (c Client) DeleteNamespace(namespace string) (tx util.Uint256, vub uint32, err error) { - method, args := c.DeleteNamespaceCall(namespace) - return c.act.SendCall(c.contract, method, args...) -} - -// DeleteNamespaceCall provides args for DeleteNamespace to use in commonclient.Transaction. -func (c Client) DeleteNamespaceCall(namespace string) (method string, args []any) { - return deleteNamespaceMethod, []any{namespace} -} - // ListNamespaces gets all namespaces. func (c Client) ListNamespaces() ([]*Namespace, error) { items, err := commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listNamespacesMethod) diff --git a/frostfsid/client/utils.go b/frostfsid/client/utils.go index 4dc0c70..e850efc 100644 --- a/frostfsid/client/utils.go +++ b/frostfsid/client/utils.go @@ -185,6 +185,7 @@ func ParseNamespaceExtended(structArr []stackitem.Item) (*NamespaceExtended, err }, nil } +// TODO: [cleanup] (#151) rewrite this method after new release. func parseNamespace(structArr []stackitem.Item, stateIndex int) (*Namespace, error) { name, err := structArr[0].TryBytes() if err != nil { @@ -192,7 +193,7 @@ func parseNamespace(structArr []stackitem.Item, stateIndex int) (*Namespace, err } nsState := Active - if len(structArr) >= stateIndex+1 { + if len(structArr) == stateIndex+1 { nsStateBytes, err := structArr[stateIndex].TryBytes() if err != nil { return nil, err diff --git a/frostfsid/config.yml b/frostfsid/config.yml index be3cfb3..609665b 100644 --- a/frostfsid/config.yml +++ b/frostfsid/config.yml @@ -72,10 +72,6 @@ events: type: String - name: state type: String - - name: DeleteNamespace - parameters: - - name: namespace - type: String - name: AddSubjectToNamespace parameters: - name: subjectAddress diff --git a/frostfsid/doc.go b/frostfsid/doc.go index a887f42..b3d5b82 100644 --- a/frostfsid/doc.go +++ b/frostfsid/doc.go @@ -21,7 +21,6 @@ FrostFSID contract does not produce notifications to process. | `c` | Int | group id counter | | `m` + [ RIPEMD160 of namespace ] + [ RIPEMD160 of subject name ] | Serialized group id int | group name to group id index | | `A` + [ subject address ] | bool | means that the wallet has been used | - | `d` + [ subject address ] + [ pk address ] | []byte{1} | link subject to extra public keys | */ diff --git a/frostfsid/frostfsid_contract.go b/frostfsid/frostfsid_contract.go index 1167041..0f4dc92 100644 --- a/frostfsid/frostfsid_contract.go +++ b/frostfsid/frostfsid_contract.go @@ -21,7 +21,6 @@ type ( // - Name: a string representing the name of the subject. // The name must match the following regex pattern: ^[\w+=,.@-]{1,64}$ // The Subject is stored in the storage as a hash(namespace) + hash(name). - // AdditionalKeys are stored in records with subject's address prefix. Subject struct { PrimaryKey interop.PublicKey AdditionalKeys []interop.PublicKey @@ -100,15 +99,10 @@ const ( groupCounterKey = 'c' namespaceGroupsNamesPrefix = 'm' addressPrefix = 'A' - subjectToAddKeyPrefix = 'd' nsActiveState = "active" - nsFrozenState = "frozen" - nsPurgeState = "purge" ) -var dummyValue = []byte{1} - func _deploy(data any, isUpdate bool) { ctx := storage.GetContext() @@ -160,23 +154,7 @@ func _deploy(data any, isUpdate bool) { storage.Put(ctx, groupCounterKey, maxGroupID) } - if args.version < common.GetVersion(0, 21, 3) { - migrateNamespacesState(ctx) - } - - if args.version < common.GetVersion(0, 21, 4) { - it := storage.Find(ctx, subjectKeysPrefix, storage.ValuesOnly) - for iterator.Next(it) { - subject := std.Deserialize(iterator.Value(it).([]byte)).(Subject) - subjAddr := contract.CreateStandardAccount(subject.PrimaryKey) - for i := 0; i < len(subject.AdditionalKeys); i++ { - storage.Put(ctx, subjectToAdditionalKeyKey(subjAddr, subject.AdditionalKeys[i]), dummyValue) - } - subject.AdditionalKeys = nil - storage.Put(ctx, subjectKeyFromAddr(subjAddr), std.Serialize(subject)) - } - } - + migrateNamespacesState(ctx) return } @@ -231,7 +209,6 @@ func Version() int { // CreateSubject creates a new subject in the specified namespace with the provided public key. func CreateSubject(ns string, key interop.PublicKey) { ctx := storage.GetContext() - checkNamespaceState(ns) checkContractOwner(ctx) if len(key) != interop.PublicKeyCompressedLen { @@ -245,7 +222,7 @@ func CreateSubject(ns string, key interop.PublicKey) { panic("subject already exists") } - saPrefix := additionalKeyToSubjectPrefix(key) + saPrefix := subjectAdditionalPrefix(key) it := storage.Find(ctx, saPrefix, storage.KeysOnly) for iterator.Next(it) { panic("key is occupied") @@ -270,7 +247,7 @@ func CreateSubject(ns string, key interop.PublicKey) { storage.Put(ctx, sKey, std.Serialize(subj)) nsSubjKey := namespaceSubjectKey(ns, addr) - storage.Put(ctx, nsSubjKey, dummyValue) + storage.Put(ctx, nsSubjKey, []byte{1}) storage.Put(ctx, allAddressKey, true) runtime.Notify("CreateSubject", interop.Hash160(addr)) @@ -293,13 +270,13 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) { panic("key is occupied") } - saKey := additionalKeyToSubjectKey(key, addr) + saKey := subjectAdditionalKey(key, addr) data := storage.Get(ctx, saKey).([]byte) if data != nil { panic("key already added") } - storage.Put(ctx, saKey, dummyValue) + storage.Put(ctx, saKey, []byte{1}) sKey := subjectKeyFromAddr(addr) data = storage.Get(ctx, sKey).([]byte) @@ -307,9 +284,9 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) { panic("address not found") } subject := std.Deserialize(data).(Subject) - checkNamespaceState(subject.Namespace) + subject.AdditionalKeys = append(subject.AdditionalKeys, key) - storage.Put(ctx, subjectToAdditionalKeyKey(addr, key), dummyValue) + storage.Put(ctx, sKey, std.Serialize(subject)) storage.Put(ctx, addressKey, true) runtime.Notify("AddSubjectKey", addr, key) } @@ -326,7 +303,7 @@ func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) { panic("incorrect public key length") } - saKey := additionalKeyToSubjectKey(key, addr) + saKey := subjectAdditionalKey(key, addr) data := storage.Get(ctx, saKey).([]byte) if data == nil { panic("key already removed") @@ -339,14 +316,17 @@ func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) { if data == nil { panic("address not found") } + subject := std.Deserialize(data).(Subject) - subjToAddKey := subjectToAdditionalKeyKey(addr, key) - data = storage.Get(ctx, subjToAddKey).([]byte) - if data == nil { - panic("key already removed") + var additionalKeys []interop.PublicKey + for i := 0; i < len(subject.AdditionalKeys); i++ { + if !common.BytesEqual(subject.AdditionalKeys[i], key) { + additionalKeys = append(additionalKeys, subject.AdditionalKeys[i]) + } } - storage.Delete(ctx, subjToAddKey) + subject.AdditionalKeys = additionalKeys + storage.Put(ctx, sKey, std.Serialize(subject)) storage.Delete(ctx, addressKey(contract.CreateStandardAccount(key))) runtime.Notify("RemoveSubjectKey", addr, key) } @@ -367,7 +347,6 @@ func SetSubjectName(addr interop.Hash160, name string) { } subject := std.Deserialize(data).(Subject) - checkNamespaceState(subject.Namespace) oldName := subject.Name subject.Name = name storage.Put(ctx, sKey, std.Serialize(subject)) @@ -393,7 +372,6 @@ func SetSubjectKV(addr interop.Hash160, key, val string) { } subject := std.Deserialize(data).(Subject) - checkNamespaceState(subject.Namespace) if subject.KV == nil { subject.KV = map[string]string{} } @@ -441,10 +419,8 @@ func DeleteSubject(addr interop.Hash160) { } subj := std.Deserialize(data).(Subject) - subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, addr) for i := 0; i < len(subj.AdditionalKeys); i++ { - storage.Delete(ctx, additionalKeyToSubjectKey(subj.AdditionalKeys[i], addr)) - storage.Delete(ctx, subjectToAdditionalKeyKey(addr, subj.AdditionalKeys[i])) + storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr)) storage.Delete(ctx, addressKey(contract.CreateStandardAccount(subj.AdditionalKeys[i]))) } storage.Delete(ctx, addressKey(addr)) @@ -466,17 +442,15 @@ func GetSubject(addr interop.Hash160) Subject { sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { - addr = getPrimaryAddr(ctx, addr) - sKey = subjectKeyFromAddr(addr) + a := getPrimaryAddr(ctx, addr) + sKey = subjectKeyFromAddr(a) data = storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } } - subj := std.Deserialize(data).(Subject) - subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, addr) - return subj + return std.Deserialize(data).(Subject) } // GetSubjectExtended retrieves the extended information of the subject with the specified address. @@ -522,18 +496,14 @@ func GetSubjectByKey(key interop.PublicKey) Subject { sKey := subjectKey(key) data := storage.Get(ctx, sKey).([]byte) if data != nil { - subj := std.Deserialize(data).(Subject) - subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, contract.CreateStandardAccount(key)) - return subj + return std.Deserialize(data).(Subject) } addr := getPrimaryAddr(ctx, contract.CreateStandardAccount(key)) sKey = subjectKeyFromAddr(addr) data = storage.Get(ctx, sKey).([]byte) if data != nil { - subj := std.Deserialize(data).(Subject) - subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, addr) - return subj + return std.Deserialize(data).(Subject) } panic("subject not found") @@ -548,20 +518,6 @@ func getPrimaryAddr(ctx storage.Context, addr interop.Hash160) interop.Hash160 { panic("subject not found") } -func getSubjectAdditionalKeys(ctx storage.Context, addr interop.Hash160) []interop.PublicKey { - var result []interop.PublicKey - subjToAddKeyPrefix := subjectToAdditionalKeyPrefix(addr) - it := storage.Find(ctx, subjToAddKeyPrefix, storage.KeysOnly|storage.RemovePrefix) - if iterator.Next(it) { - key := iterator.Value(it).([]byte) - if len(key) < interop.PublicKeyCompressedLen { - panic("invalid subject additional key") - } - result = append(result, interop.PublicKey(key[:interop.PublicKeyCompressedLen])) - } - return result -} - // GetSubjectByName retrieves the subject with the specified name within the given namespace. func GetSubjectByName(ns, name string) Subject { key := GetSubjectKeyByName(ns, name) @@ -639,36 +595,6 @@ func CreateNamespace(ns string) { runtime.Notify("CreateNamespace", ns) } -// DeleteNamespace idempotently removes a namespace with the specified name. -func DeleteNamespace(ns string) { - ctx := storage.GetContext() - checkContractOwner(ctx) - - nsKey := namespaceKey(ns) - data := storage.Get(ctx, nsKey).([]byte) - if data == nil { - return - } - - namespace := std.Deserialize(data).(Namespace) - if namespace.State != nsPurgeState { - panic("namespace should be in 'purge' state for deletion") - } - - it := storage.Find(ctx, groupPrefix(ns), storage.KeysOnly) - if iterator.Next(it) { - panic("can't delete non-empty namespace: groups still present") - } - - it = storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly) - if iterator.Next(it) { - panic("can't delete non-empty namespace: users still present") - } - - storage.Delete(ctx, nsKey) - runtime.Notify("DeleteNamespace", ns) -} - // UpdateNamespace updates existing namespace. func UpdateNamespace(ns string, state string) { ctx := storage.GetContext() @@ -742,7 +668,6 @@ func ListNamespaceSubjects(ns string) iterator.Iterator { // CreateGroup creates a new group within the specified namespace. func CreateGroup(ns, group string) int { ctx := storage.GetContext() - checkNamespaceState(ns) checkContractOwner(ctx) if group == "" { @@ -856,7 +781,6 @@ func SetGroupName(ns string, groupID int, name string) { } gr := std.Deserialize(data).(Group) - checkNamespaceState(gr.Namespace) oldName := gr.Name gr.Name = name storage.Put(ctx, gKey, std.Serialize(gr)) @@ -878,7 +802,6 @@ func SetGroupKV(ns string, groupID int, key, val string) { } gr := std.Deserialize(data).(Group) - checkNamespaceState(gr.Namespace) if gr.KV == nil { gr.KV = map[string]string{} } @@ -926,7 +849,6 @@ func AddSubjectToGroup(addr interop.Hash160, groupID int) { panic("subject not found") } subject := std.Deserialize(data).(Subject) - checkNamespaceState(subject.Namespace) gKey := groupKey(subject.Namespace, groupID) data = storage.Get(ctx, gKey).([]byte) @@ -940,7 +862,7 @@ func AddSubjectToGroup(addr interop.Hash160, groupID int) { } gsKey := groupSubjectKey(subject.Namespace, groupID, addr) - storage.Put(ctx, gsKey, dummyValue) + storage.Put(ctx, gsKey, []byte{1}) runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, groupID) } @@ -1109,25 +1031,15 @@ func subjectKeyFromAddr(addr interop.Hash160) []byte { return append([]byte{subjectKeysPrefix}, addr...) } -func additionalKeyToSubjectKey(additionalKey interop.PublicKey, primeAddr interop.Hash160) []byte { - return append(additionalKeyToSubjectPrefix(additionalKey), primeAddr...) +func subjectAdditionalKey(additionalKey interop.PublicKey, primeAddr interop.Hash160) []byte { + return append(subjectAdditionalPrefix(additionalKey), primeAddr...) } -func additionalKeyToSubjectPrefix(additionalKey interop.PublicKey) []byte { +func subjectAdditionalPrefix(additionalKey interop.PublicKey) []byte { addr := contract.CreateStandardAccount(additionalKey) return append([]byte{additionalKeysPrefix}, addr...) } -// subjectToAdditionalKeyKey returns 'd' + [20]byte subjectAddr + [33]byte additionalKey. -func subjectToAdditionalKeyKey(subjectAddr interop.Hash160, additionalKey interop.PublicKey) []byte { - return append(subjectToAdditionalKeyPrefix(subjectAddr), additionalKey...) -} - -// subjectToAdditionalKeyPrefix returns 'd' + [20]byte subjectAddr. -func subjectToAdditionalKeyPrefix(subjectAddr interop.Hash160) []byte { - return append([]byte{subjectToAddKeyPrefix}, subjectAddr...) -} - func namespaceKey(ns string) []byte { return namespaceKeyFromHash(ripemd160Hash(ns)) } @@ -1214,6 +1126,7 @@ func addressKey(address []byte) []byte { return append([]byte{addressPrefix}, address...) } +// TODO: [cleanup] (#151) remove this migration after new release. func migrateNamespacesState(ctx storage.Context) { it := storage.Find(ctx, []byte{namespaceKeysPrefix}, storage.None) @@ -1236,10 +1149,3 @@ func migrateNamespacesState(ctx storage.Context) { storage.Put(ctx, string(kv.Key), namespaceData) } } - -func checkNamespaceState(name string) { - ns := GetNamespace(name) - if ns.State == nsFrozenState || ns.State == nsPurgeState { - panic("namespace is non-active") - } -} diff --git a/nns/namestate.go b/nns/namestate.go index ea186c8..72bbf94 100644 --- a/nns/namestate.go +++ b/nns/namestate.go @@ -7,14 +7,19 @@ import ( // NameState represents domain name state. type NameState struct { - Owner interop.Hash160 - Name string - // Expiration field used to contain wall-clock time of a domain expiration. - // It is preserved for backwards compatibility, but is unused by the contract and should be ignored. + Owner interop.Hash160 + Name string Expiration int64 Admin interop.Hash160 } +// ensureNotExpired panics if domain name is expired. +func (n NameState) ensureNotExpired() { + if int64(runtime.GetTime()) >= n.Expiration { + panic("name has expired") + } +} + // checkAdmin panics if script container is not signed by the domain name admin. func (n NameState) checkAdmin() { if runtime.CheckWitness(n.Owner) { diff --git a/nns/nns_contract.go b/nns/nns_contract.go index 9185e45..d4bc83b 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -148,7 +148,8 @@ func Properties(tokenID []byte) map[string]any { ctx := storage.GetReadOnlyContext() ns := getNameState(ctx, tokenID) return map[string]any{ - "name": ns.Name, + "name": ns.Name, + "expiration": ns.Expiration, } } @@ -307,6 +308,7 @@ func extractCnametgt(ctx storage.Context, name, domain string) string { // checkParent returns parent domain or empty string if domain not found. func checkParent(ctx storage.Context, fragments []string) string { + now := int64(runtime.GetTime()) last := len(fragments) - 1 name := fragments[last] parent := "" @@ -318,6 +320,10 @@ func checkParent(ctx storage.Context, fragments []string) string { if nsBytes == nil { continue } + ns := std.Deserialize(nsBytes.([]byte)).(NameState) + if now >= ns.Expiration { + panic("domain expired: " + name) + } parent = name } return parent @@ -384,15 +390,19 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...)) if nsBytes != nil { ns := std.Deserialize(nsBytes.([]byte)).(NameState) + if int64(runtime.GetTime()) < ns.Expiration { + return false + } oldOwner = ns.Owner updateBalance(ctx, []byte(name), oldOwner, -1) } else { updateTotalSupply(ctx, +1) } ns := NameState{ - Owner: owner, - Name: name, - Expiration: 0, + Owner: owner, + Name: name, + // NNS expiration is in milliseconds + Expiration: int64(runtime.GetTime() + expire*1000), } checkAvailableGlobalDomain(ctx, name) @@ -405,6 +415,18 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str return true } +// Renew increases domain expiration date. +func Renew(name string) int64 { + checkDomainNameLength(name) + runtime.BurnGas(GetPrice()) + ctx := storage.GetContext() + ns := getNameState(ctx, []byte(name)) + ns.checkAdmin() + ns.Expiration += millisecondsInYear + putNameState(ctx, ns) + return ns.Expiration +} + // UpdateSOA updates soa record. func UpdateSOA(name, email string, refresh, retry, expire, ttl int) { checkDomainNameLength(name) @@ -709,7 +731,9 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState { if nsBytes == nil { panic("token not found") } - return std.Deserialize(nsBytes.([]byte)).(NameState) + ns := std.Deserialize(nsBytes.([]byte)).(NameState) + ns.ensureNotExpired() + return ns } // putNameState stores domain name state. @@ -777,7 +801,7 @@ func addRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, ns := NameState{ Name: globalDomain, Owner: nsOriginal.Owner, - Expiration: 0, + Expiration: nsOriginal.Expiration, Admin: nsOriginal.Admin, } @@ -1095,13 +1119,16 @@ func tokenIDFromName(name string) string { ctx := storage.GetReadOnlyContext() sum := 0 - l := len(fragments) + l := len(fragments) - 1 for i := 0; i < l; i++ { tokenKey := getTokenKey([]byte(name[sum:])) nameKey := append([]byte{prefixName}, tokenKey...) nsBytes := storage.Get(ctx, nameKey) if nsBytes != nil { - return name[sum:] + ns := std.Deserialize(nsBytes.([]byte)).(NameState) + if int64(runtime.GetTime()) < ns.Expiration { + return name[sum:] + } } sum += len(fragments[i]) + 1 } diff --git a/policy/policy_contract.go b/policy/policy_contract.go index 10f8cd7..8959859 100644 --- a/policy/policy_contract.go +++ b/policy/policy_contract.go @@ -2,13 +2,9 @@ package policy import ( "git.frostfs.info/TrueCloudLab/frostfs-contract/common" - "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid" - "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "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/iterator" "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/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/storage" ) @@ -40,11 +36,6 @@ const ( ErrNotAuthorized = "none of the signers is authorized to change the contract" ) -const ( - purgeNsState = "purge" - frozenNsState = "frozen" -) - // _deploy function sets up initial list of inner ring public keys. func _deploy(data any, isUpdate bool) { if isUpdate { @@ -59,7 +50,7 @@ func _deploy(data any, isUpdate bool) { ctx := storage.GetContext() if args.Admin != nil { if len(args.Admin) != 20 { - panic("invalid admin hash length") + panic("invaliad admin hash length") } storage.Put(ctx, []byte{ownerKeyPrefix}, args.Admin) } @@ -151,51 +142,9 @@ func mapToNumericCreateIfNotExists(ctx storage.Context, kind Kind, name []byte) return numericID.(int) } -func checkChainNamespace(entity Kind, name string) { - if entity != Namespace { - return - } - frostfsidAddr := getContractHash(nns.FrostfsIDNNSName) - if frostfsidAddr == nil || management.GetContract(frostfsidAddr) == nil { - panic("could not get frostfsid contract") - } - ns := contract.Call(frostfsidAddr, "getNamespace", contract.ReadOnly, name).(frostfsid.Namespace) - if ns.State == purgeNsState || ns.State == frozenNsState { - panic("namespace is non-active") - } -} - -// getContractHash returns nil when it can't resolve contract name, -// so custom error message can be thrown. -func getContractHash(name string) interop.Hash160 { - nnsContract := management.GetContractByID(1) - records := contract.Call(nnsContract.Hash, "getRecords", contract.ReadOnly, name, nns.TXT).([]string) - for _, record := range records { - contractHash := readContractHashFromNNSRecord(record) - if contractHash != nil { - return contractHash - } - } - return nil -} - -func readContractHashFromNNSRecord(nnsResponse string) interop.Hash160 { - // 40 is size of hex encoded contract hash as string - if len(nnsResponse) == 40 { - return nil - } - - decoded := std.Base58Decode([]byte(nnsResponse)) - if len(decoded) != common.AddressLen || management.GetContract(decoded[1:21]) == nil { - return nil - } - return decoded[1:21] -} - func AddChain(entity Kind, entityName string, name []byte, chain []byte) { ctx := storage.GetContext() checkAuthorization(ctx) - checkChainNamespace(entity, entityName) entityNameBytes := mapToNumericCreateIfNotExists(ctx, entity, []byte(entityName)) key := storageKey(entity, entityNameBytes, name) diff --git a/rpcclient/frostfsid/client.go b/rpcclient/frostfsid/client.go index 498b472..19ecaf1 100644 --- a/rpcclient/frostfsid/client.go +++ b/rpcclient/frostfsid/client.go @@ -70,11 +70,6 @@ type UpdateNamespaceEvent struct { State string } -// DeleteNamespaceEvent represents "DeleteNamespace" event emitted by the contract. -type DeleteNamespaceEvent struct { - Namespace string -} - // AddSubjectToNamespaceEvent represents "AddSubjectToNamespace" event emitted by the contract. type AddSubjectToNamespaceEvent struct { SubjectAddress util.Uint160 @@ -494,28 +489,6 @@ func (c *Contract) DeleteGroupKVUnsigned(ns string, groupID *big.Int, key string return c.actor.MakeUnsignedCall(c.hash, "deleteGroupKV", nil, ns, groupID, key) } -// DeleteNamespace creates a transaction invoking `deleteNamespace` method of the contract. -// This transaction is signed and immediately sent to the network. -// The values returned are its hash, ValidUntilBlock value and error if any. -func (c *Contract) DeleteNamespace(ns string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "deleteNamespace", ns) -} - -// DeleteNamespaceTransaction creates a transaction invoking `deleteNamespace` method of the contract. -// This transaction is signed, but not sent to the network, instead it's -// returned to the caller. -func (c *Contract) DeleteNamespaceTransaction(ns string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "deleteNamespace", ns) -} - -// DeleteNamespaceUnsigned creates a transaction invoking `deleteNamespace` method of the contract. -// This transaction is not signed, it's simply returned to the caller. -// Any fields of it that do not affect fees can be changed (ValidUntilBlock, -// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. -func (c *Contract) DeleteNamespaceUnsigned(ns string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "deleteNamespace", nil, ns) -} - // DeleteSubject creates a transaction invoking `deleteSubject` method of the contract. // This transaction is signed and immediately sent to the network. // The values returned are its hash, ValidUntilBlock value and error if any. @@ -1421,67 +1394,6 @@ func (e *UpdateNamespaceEvent) FromStackItem(item *stackitem.Array) error { return nil } -// DeleteNamespaceEventsFromApplicationLog retrieves a set of all emitted events -// with "DeleteNamespace" name from the provided [result.ApplicationLog]. -func DeleteNamespaceEventsFromApplicationLog(log *result.ApplicationLog) ([]*DeleteNamespaceEvent, error) { - if log == nil { - return nil, errors.New("nil application log") - } - - var res []*DeleteNamespaceEvent - for i, ex := range log.Executions { - for j, e := range ex.Events { - if e.Name != "DeleteNamespace" { - continue - } - event := new(DeleteNamespaceEvent) - err := event.FromStackItem(e.Item) - if err != nil { - return nil, fmt.Errorf("failed to deserialize DeleteNamespaceEvent from stackitem (execution #%d, event #%d): %w", i, j, err) - } - res = append(res, event) - } - } - - return res, nil -} - -// FromStackItem converts provided [stackitem.Array] to DeleteNamespaceEvent or -// returns an error if it's not possible to do to so. -func (e *DeleteNamespaceEvent) FromStackItem(item *stackitem.Array) error { - if item == nil { - return errors.New("nil item") - } - arr, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not an array") - } - if len(arr) != 1 { - return errors.New("wrong number of structure elements") - } - - var ( - index = -1 - err error - ) - index++ - e.Namespace, err = func(item stackitem.Item) (string, error) { - b, err := item.TryBytes() - if err != nil { - return "", err - } - if !utf8.Valid(b) { - return "", errors.New("not a UTF-8 string") - } - return string(b), nil - }(arr[index]) - if err != nil { - return fmt.Errorf("field Namespace: %w", err) - } - - return nil -} - // AddSubjectToNamespaceEventsFromApplicationLog retrieves a set of all emitted events // with "AddSubjectToNamespace" name from the provided [result.ApplicationLog]. func AddSubjectToNamespaceEventsFromApplicationLog(log *result.ApplicationLog) ([]*AddSubjectToNamespaceEvent, error) { diff --git a/rpcclient/nns/client.go b/rpcclient/nns/client.go index 43c0d0a..adfc41e 100644 --- a/rpcclient/nns/client.go +++ b/rpcclient/nns/client.go @@ -286,6 +286,28 @@ func (c *Contract) RegisterUnsigned(name string, owner util.Uint160, email strin return c.actor.MakeUnsignedRun(script, nil) } +// Renew creates a transaction invoking `renew` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Renew(name string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "renew", name) +} + +// RenewTransaction creates a transaction invoking `renew` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RenewTransaction(name string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "renew", name) +} + +// RenewUnsigned creates a transaction invoking `renew` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RenewUnsigned(name string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name) +} + // SetAdmin creates a transaction invoking `setAdmin` method of the contract. // This transaction is signed and immediately sent to the network. // The values returned are its hash, ValidUntilBlock value and error if any. diff --git a/tests/frostfsid_client_test.go b/tests/frostfsid_client_test.go index d1e21b2..5303baf 100644 --- a/tests/frostfsid_client_test.go +++ b/tests/frostfsid_client_test.go @@ -189,79 +189,6 @@ func TestFrostFSID_Client_NamespaceManagement(t *testing.T) { subjects, err = ffsid.cli.ListNamespaceSubjects(namespace) require.NoError(t, err) require.Empty(t, subjects) - - namespace2 := "namespace2" - ffsid.a.await(ffsid.cli.CreateNamespace(namespace2)) - ns1, err := ffsid.cli.GetNamespace(namespace) - require.NoError(t, err) - require.Equal(t, namespace, ns1.Name) - require.Equal(t, client.Active, ns1.State) - - nsExt2, err := ffsid.cli.GetNamespaceExtended(namespace2) - require.NoError(t, err) - require.Equal(t, namespace2, nsExt2.Name) - require.Equal(t, client.Active, nsExt2.State) - - ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Frozen)) - nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2) - require.NoError(t, err) - require.Equal(t, namespace2, nsExt2.Name) - require.Equal(t, client.Frozen, nsExt2.State) - - _, _, err = ffsid.cli.DeleteNamespace(namespace2) - require.ErrorContains(t, err, "namespace should be in 'purge' state for deletion") - - ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Active)) - nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2) - require.NoError(t, err) - require.Equal(t, client.Active, nsExt2.State) - - subjKey2, subjAddr2 := newKey(t) - ffsid.a.await(ffsid.cli.CreateSubject(namespace2, subjKey2.PublicKey())) - - subj2, err := ffsid.cli.GetSubject(subjAddr2) - require.NoError(t, err) - require.Equal(t, namespace2, subj2.Namespace) - - ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Purge)) - nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2) - require.NoError(t, err) - require.Equal(t, client.Purge, nsExt2.State) - - _, _, err = ffsid.cli.DeleteNamespace(namespace2) - require.ErrorContains(t, err, "can't delete non-empty namespace: users still present") - - ffsid.a.await(ffsid.cli.DeleteSubject(subjAddr2)) - - ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Active)) - nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2) - require.NoError(t, err) - require.Equal(t, client.Active, nsExt2.State) - - groupName := "ns_group" - ffsid.a.await(ffsid.cli.CreateGroup(namespace2, groupName)) - group, err := ffsid.cli.GetGroupByName(namespace2, groupName) - require.NoError(t, err) - - ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Purge)) - nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2) - require.NoError(t, err) - require.Equal(t, client.Purge, nsExt2.State) - - _, _, err = ffsid.cli.DeleteNamespace(namespace2) - require.ErrorContains(t, err, "can't delete non-empty namespace: groups still present") - - ffsid.a.await(ffsid.cli.DeleteGroup(namespace2, group.ID)) - - ffsid.a.await(ffsid.cli.DeleteNamespace(namespace2)) - _, err = ffsid.cli.GetNamespace(namespace2) - require.ErrorContains(t, err, "not found") - - namespace3 := "namespace3" - _, err = ffsid.cli.GetNamespace(namespace3) - require.ErrorContains(t, err, "not found") - _, _, err = ffsid.cli.DeleteNamespace(namespace3) - require.NoError(t, err) } func TestFrostFSID_Client_DefaultNamespace(t *testing.T) { @@ -653,25 +580,3 @@ func prettyPrintExtendedSubjects(subjects []*client.SubjectExtended) { fmt.Println(sb.String()) } } - -func TestFrostfsID_ConcurrentAddSubjectKey(t *testing.T) { - f := newFrostFSIDInvoker(t) - - newKey := func(t *testing.T) *keys.PrivateKey { - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - return pk - } - - subjKey := newKey(t) - subjKeyAddr := subjKey.PublicKey().GetScriptHash() - invoker := f.OwnerInvoker() - invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjKey.PublicKey().Bytes()) - - additionalKey1 := newKey(t) - additionalKey2 := newKey(t) - tx1 := invoker.PrepareInvoke(t, addSubjectKeyMethod, subjKeyAddr, additionalKey1.PublicKey().Bytes()) - tx2 := invoker.PrepareInvoke(t, addSubjectKeyMethod, subjKeyAddr, additionalKey2.PublicKey().Bytes()) - - invoker.AddBlockCheckHalt(t, tx1, tx2) -} diff --git a/tests/frostfsid_test.go b/tests/frostfsid_test.go index f91865b..bb9fc17 100644 --- a/tests/frostfsid_test.go +++ b/tests/frostfsid_test.go @@ -24,10 +24,7 @@ import ( const frostfsidPath = "../frostfsid" -const ( - defaultNamespace = "" - customNamespace = "custom" -) +const defaultNamespace = "" const ( setAdminMethod = "setAdmin" @@ -50,7 +47,6 @@ const ( getNamespaceMethod = "getNamespace" getNamespaceExtendedMethod = "getNamespaceExtended" updateNamespaceMethod = "updateNamespace" - deleteNamespaceMethod = "deleteNamespace" listNamespacesMethod = "listNamespaces" listNamespaceSubjectsMethod = "listNamespaceSubjects" @@ -70,16 +66,7 @@ const ( nsActiveState = "active" ) -const ( - frozenState = "frozen" - purgeState = "purge" - namespaceNonActive = "namespace is non-active" - notWitnessedError = "not witnessed" - notFoundError = "namespace not found" - cantDeleteNonEmptyNamespceGroupsPresent = "can't delete non-empty namespace: groups still present" - cantDeleteNonEmptyNamespceUsersPresent = "can't delete non-empty namespace: users still present" - namespaceShouldBeInPurgeStateError = "namespace should be in 'purge' state for deletion" -) +const notWitnessedError = "not witnessed" type testFrostFSIDInvoker struct { e *neotest.Executor @@ -319,56 +306,6 @@ func TestFrostFSID_SubjectManagement(t *testing.T) { require.ElementsMatch(t, addresses, []util.Uint160{subjKeyAddr, newSubjKey.PublicKey().GetScriptHash()}) }) - t.Run("subject operations for non-active namespace", func(t *testing.T) { - invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, customNamespace) - - baseKey, err := keys.NewPrivateKey() - require.NoError(t, err) - baseAddr, baseBytes := baseKey.PublicKey().GetScriptHash(), baseKey.PublicKey().Bytes() - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - - invoker.InvokeFail(t, namespaceNonActive, createSubjectMethod, customNamespace, baseBytes) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, createSubjectMethod, customNamespace, baseBytes) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, "active") - invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, customNamespace, baseBytes) - - t.Run("addSubjectKey", func(t *testing.T) { - newSubjKey, err := keys.NewPrivateKey() - require.NoError(t, err) - keyBytes := newSubjKey.PublicKey().Bytes() - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, addSubjectKeyMethod, baseAddr, keyBytes) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, addSubjectKeyMethod, baseAddr, keyBytes) - }) - - t.Run("setSubjectKV", func(t *testing.T) { - const key, val = "key", "val" - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, setSubjectKVMethod, baseAddr, key, val) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, setSubjectKVMethod, baseAddr, key, val) - }) - - t.Run("setSubjectName", func(t *testing.T) { - const login = "testlogin" - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, setSubjectNameMethod, baseAddr, login) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, setSubjectNameMethod, baseAddr, login) - }) - }) - anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectMethod, subjKeyAddr) invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjKeyAddr) @@ -577,59 +514,6 @@ func TestFrostFSID_NamespaceManagement(t *testing.T) { require.Equal(t, namespace, ns.Name) require.Equal(t, "frozen", ns.State) }) - - t.Run("delete namespace", func(t *testing.T) { - namespace3 := "some-namespace3" - invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, namespace3) - - s, err = invoker.TestInvoke(t, getNamespaceMethod, namespace3) - require.NoError(t, err) - - ns := parseNamespace(t, s.Pop().Item()) - require.Equal(t, namespace3, ns.Name) - - t.Run("delete existing namespace not in a 'purge' state", func(t *testing.T) { - anonInvoker.InvokeFail(t, notWitnessedError, deleteNamespaceMethod, namespace3) - invoker.InvokeFail(t, namespaceShouldBeInPurgeStateError, deleteNamespaceMethod, namespace3) - }) - - subjKey, err := keys.NewPrivateKey() - subjKeyAddr := subjKey.PublicKey().GetScriptHash() - require.NoError(t, err) - invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, ns.Name, subjKey.PublicKey().Bytes()) - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, namespace3, "purge") - - t.Run("delete namespace with user fails", func(t *testing.T) { - invoker.InvokeFail(t, cantDeleteNonEmptyNamespceUsersPresent, deleteNamespaceMethod, namespace3) - }) - - invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjKeyAddr) - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, namespace3, "active") - - groupID1 := int64(1) - groupName1 := "group1" - invoker.Invoke(t, stackitem.Make(groupID1), createGroupMethod, ns.Name, groupName1) - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, namespace3, "purge") - - t.Run("delete namespace with group fails", func(t *testing.T) { - invoker.InvokeFail(t, cantDeleteNonEmptyNamespceGroupsPresent, deleteNamespaceMethod, namespace3) - }) - - invoker.Invoke(t, stackitem.Null{}, deleteGroupMethod, ns.Name, groupID1) - - t.Run("delete existing namespace", func(t *testing.T) { - anonInvoker.InvokeFail(t, notWitnessedError, deleteNamespaceMethod, namespace3) - invoker.Invoke(t, stackitem.Null{}, deleteNamespaceMethod, namespace3) - invoker.InvokeFail(t, notFoundError, getNamespaceMethod, namespace3) - }) - - t.Run("delete non-existing namespace", func(t *testing.T) { - nonExistingNamespace := "non-existing-namespace" - invoker.InvokeFail(t, notFoundError, getNamespaceMethod, nonExistingNamespace) - anonInvoker.InvokeFail(t, notWitnessedError, deleteNamespaceMethod, nonExistingNamespace) - invoker.Invoke(t, stackitem.Null{}, deleteNamespaceMethod, nonExistingNamespace) - }) - }) } func TestFrostFSID_GroupManagement(t *testing.T) { @@ -751,50 +635,6 @@ func TestFrostFSID_GroupManagement(t *testing.T) { groups := parseGroups(t, readIteratorAll(s)) require.Empty(t, groups) }) - - t.Run("operations with non-active namespace", func(t *testing.T) { - invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, customNamespace) - - customGroupID := int64(2) - customGroupName := "customGroup" - invoker.Invoke(t, stackitem.Make(customGroupID), createGroupMethod, customNamespace, customGroupName) - - subjKey, err := keys.NewPrivateKey() - require.NoError(t, err) - invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, customNamespace, subjKey.PublicKey().Bytes()) - - t.Run("createGroup", func(t *testing.T) { - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, createGroupMethod, customNamespace, customGroupName) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, createGroupMethod, customNamespace, customGroupName) - }) - - t.Run("addSubjectToGroup", func(t *testing.T) { - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), customGroupID) - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), customGroupID) - }) - - t.Run("setGroupKV", func(t *testing.T) { - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, setGroupKVMethod, customNamespace, customGroupID, client.IAMARNKey, "arn") - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, setGroupKVMethod, customNamespace, customGroupID, client.IAMARNKey, "arn") - }) - - t.Run("setGroupName", func(t *testing.T) { - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState) - invoker.InvokeFail(t, namespaceNonActive, setGroupNameMethod, customNamespace, customGroupID, "newCustomGroup") - - invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState) - invoker.InvokeFail(t, namespaceNonActive, setGroupNameMethod, customNamespace, customGroupID, "newCustomGroup") - }) - }) } func TestAdditionalKeyFromPrimarySubject(t *testing.T) { diff --git a/tests/nns_test.go b/tests/nns_test.go index 24f5761..56ed98a 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" "path" + "strings" "testing" "time" @@ -269,58 +270,7 @@ func TestNNSRegister(t *testing.T) { expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))}) c.CheckTxNotificationEvent(t, tx, 4, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteDomain", Item: expected}) - c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.SOA)) -} - -func TestDeleteRecords_SubdomainNoRegister(t *testing.T) { - c := newNNSInvoker(t, true) - - refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) - c.Invoke(t, true, "register", - "test.com", c.CommitteeHash, - "myemail@frostfs.info", refresh, retry, expire, ttl) - - checkRecords := func(t *testing.T, domain string, typ nns.RecordType, expected ...string) { - s, err := c.TestInvoke(t, "getRecords", domain, int64(typ)) - require.NoError(t, err) - - if len(expected) == 0 { - _, ok := s.Pop().Item().(stackitem.Null) - require.True(t, ok, "expected 0 records") - return - } - - arr, ok := s.Pop().Value().([]stackitem.Item) - require.True(t, ok, "expected an array '%s' %d", domain, typ) - - actual := make([]string, len(arr)) - for i := range actual { - b, err := arr[i].TryBytes() - require.NoError(t, err) - actual[i] = string(b) - } - - require.ElementsMatch(t, expected, actual) - } - - c.Invoke(t, stackitem.Null{}, "addRecord", "a.test.com", int64(nns.TXT), "recA1") - c.Invoke(t, stackitem.Null{}, "addRecord", "a.test.com", int64(nns.TXT), "recA2") - c.Invoke(t, stackitem.Null{}, "addRecord", "b.test.com", int64(nns.TXT), "recB") - c.Invoke(t, stackitem.Null{}, "addRecord", "test.com", int64(nns.TXT), "recTop") - - { // Delete subdomain records. - c.Invoke(t, stackitem.Null{}, "deleteRecords", "a.test.com", int64(nns.TXT)) - checkRecords(t, "test.com", nns.TXT, "recTop") - checkRecords(t, "a.test.com", nns.TXT) - checkRecords(t, "b.test.com", nns.TXT, "recB") - } - - { // Delete domain records. - c.Invoke(t, stackitem.Null{}, "deleteRecords", "test.com", int64(nns.TXT)) - checkRecords(t, "test.com", nns.TXT) - checkRecords(t, "a.test.com", nns.TXT) - checkRecords(t, "b.test.com", nns.TXT, "recB") - } + c.InvokeFail(t, "token not found", "getRecords", "testdomain.com", int64(nns.SOA)) } func TestDeleteDomain(t *testing.T) { @@ -437,10 +387,6 @@ func TestTLDRecord(t *testing.T) { result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))} c.Invoke(t, result, "resolve", "com", int64(nns.A)) - - t.Run("subdomain", func(t *testing.T) { - c.Invoke(t, stackitem.Null{}, "addRecord", "a.com", int64(nns.TXT), "test=frostfs") - }) } func TestNNSRegisterMulti(t *testing.T) { @@ -554,6 +500,45 @@ func TestNNSGetAllRecords(t *testing.T) { require.False(t, iter.Next()) } +func TestExpiration(t *testing.T) { + c := newNNSInvoker(t, true) + + refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*10), int64(104) + c.Invoke(t, true, "register", + "testdomain.com", c.CommitteeHash, + "myemail@frostfs.info", refresh, retry, expire, ttl) + + checkProperties := func(t *testing.T, expiration uint64) { + expected := stackitem.NewMapWithValue([]stackitem.MapElement{ + {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")}, + {Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)}, + }) + s, err := c.TestInvoke(t, "properties", "testdomain.com") + require.NoError(t, err) + require.Equal(t, expected.Value(), s.Top().Item().Value()) + } + + top := c.TopBlock(t) + expiration := top.Timestamp + uint64(expire*1000) + checkProperties(t, expiration) + + b := c.NewUnsignedBlock(t) + b.Timestamp = expiration - 2 // test invoke is done with +1 timestamp + require.NoError(t, c.Chain.AddBlock(c.SignBlock(b))) + checkProperties(t, expiration) + + b = c.NewUnsignedBlock(t) + b.Timestamp = expiration - 1 + require.NoError(t, c.Chain.AddBlock(c.SignBlock(b))) + + _, err := c.TestInvoke(t, "properties", "testdomain.com") + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "name has expired")) + + c.InvokeFail(t, "name has expired", "getAllRecords", "testdomain.com") + c.InvokeFail(t, "name has expired", "ownerOf", "testdomain.com") +} + func TestNNSSetAdmin(t *testing.T) { c := newNNSInvoker(t, true) @@ -655,6 +640,31 @@ func TestNNSIsAvailable(t *testing.T) { c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255)) } +func TestNNSRenew(t *testing.T) { + c := newNNSInvoker(t, true) + + acc := c.NewAccount(t) + c1 := c.WithSigners(c.Committee, acc) + refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104) + c1.Invoke(t, true, "register", + "testdomain.com", c.CommitteeHash, + "myemail@frostfs.info", refresh, retry, expire, ttl) + + const msPerYear = 365 * 24 * time.Hour / time.Millisecond + b := c.TopBlock(t) + ts := b.Timestamp + uint64(expire*1000) + uint64(msPerYear) + + cAcc := c.WithSigners(acc) + cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com") + c1.Invoke(t, ts, "renew", "testdomain.com") + expected := stackitem.NewMapWithValue([]stackitem.MapElement{ + {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")}, + {Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)}, + }) + cAcc.Invoke(t, expected, "properties", "testdomain.com") + c.InvokeFail(t, "domain name too long", "renew", getTooLongDomainName(255)) +} + func TestNNSResolve(t *testing.T) { c := newNNSInvoker(t, true) diff --git a/tests/policy_test.go b/tests/policy_test.go index 361d435..1355c5a 100644 --- a/tests/policy_test.go +++ b/tests/policy_test.go @@ -5,76 +5,35 @@ import ( "path" "testing" - "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" - "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) const policyPath = "../policy" -type policyContracts struct { - policy *neotest.ContractInvoker - frostfsid *neotest.ContractInvoker -} - -func newPolicyInvokers(t *testing.T) *policyContracts { - e := newExecutor(t) - - n := deployNNSContract(t, e) - ffid := deployFrostfsid(t, e) - polic := deployPolicyContract(t, e) - - n.Invoke(t, true, "register", - nns.FrostfsIDNNSName, n.CommitteeHash, - "myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL) - n.Invoke(t, stackitem.Null{}, "addRecord", - nns.FrostfsIDNNSName, int64(nns.TXT), ffid.Hash.StringLE()) - n.Invoke(t, stackitem.Null{}, "addRecord", - nns.FrostfsIDNNSName, int64(nns.TXT), address.Uint160ToString(ffid.Hash)) - - return &policyContracts{ - policy: polic, - frostfsid: ffid, - } -} - -func deployNNSContract(t *testing.T, e *neotest.Executor) *neotest.ContractInvoker { - ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) - e.DeployContract(t, ctrNNS, nil) - - n := e.CommitteeInvoker(ctrNNS.Hash) - - return n -} - -func deployFrostfsid(t *testing.T, e *neotest.Executor) *neotest.ContractInvoker { - acc, err := wallet.NewAccount() - require.NoError(t, err) - args := make([]any, 5) - args[0] = acc.ScriptHash() - frostfsID := neotest.CompileFile(t, e.CommitteeHash, frostfsidPath, path.Join(frostfsidPath, "config.yml")) - e.DeployContract(t, frostfsID, args) - return e.CommitteeInvoker(frostfsID.Hash) -} - -func deployPolicyContract(t *testing.T, e *neotest.Executor) *neotest.ContractInvoker { +func deployPolicyContract(t *testing.T, e *neotest.Executor) util.Uint160 { cfgPath := path.Join(policyPath, "config.yml") c := neotest.CompileFile(t, e.CommitteeHash, policyPath, cfgPath) e.DeployContract(t, c, []any{nil}) - return e.CommitteeInvoker(c.Hash) + return c.Hash +} + +func newPolicyInvoker(t *testing.T) *neotest.ContractInvoker { + e := newExecutor(t) + h := deployPolicyContract(t, e) + return e.CommitteeInvoker(h) } func TestPolicy(t *testing.T) { - c := newPolicyInvokers(t) + e := newPolicyInvoker(t) - checkChainsIteratorByPrefix(t, c.policy, policy.Namespace, "mynamespace", "ingress", [][]byte{}) - checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress", [][]byte{}) + checkChainsIteratorByPrefix(t, e, policy.Namespace, "mynamespace", "ingress", [][]byte{}) + checkChainsIteratorByPrefix(t, e, policy.Container, "cnr1", "ingress", [][]byte{}) // Policies are opaque to the contract and are just raw bytes to store. p1 := []byte("chain1") @@ -82,97 +41,82 @@ func TestPolicy(t *testing.T) { p3 := []byte("chain3") p33 := []byte("chain33") - c.frostfsid.Invoke(t, stackitem.Null{}, "createNamespace", "mynamespace") + e.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) + checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) + checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) + checkChains(t, e, "mynamespace", "", "all", nil) - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) - checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) - checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1}) - checkChains(t, c.policy, "mynamespace", "", "all", nil) + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule2", p2) + checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) + checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) + checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) // Only namespace chains. + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2}) + checkChains(t, e, "mynamespace", "cnr1", "all", nil) // No chains attached to 'all'. + checkChains(t, e, "mynamespace", "cnr2", "ingress", [][]byte{p1}) // Only namespace, no chains for the container. - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule2", p2) - checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) - checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) - checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1}) // Only namespace chains. - checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2}) - checkChains(t, c.policy, "mynamespace", "cnr1", "all", nil) // No chains attached to 'all'. - checkChains(t, c.policy, "mynamespace", "cnr2", "ingress", [][]byte{p1}) // Only namespace, no chains for the container. + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p3) + checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) + checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p3}) - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p3) - checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) - checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) - checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p3}) + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) + checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) + checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) + checkChain(t, e, policy.Container, "cnr1", "ingress:myrule3", p33) + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p33}) // Override chain. + checkChainsByPrefix(t, e, policy.Container, "cnr1", "", [][]byte{p2, p33}) + checkChainsByPrefix(t, e, policy.IAM, "", "", nil) + checkChainKeys(t, e, policy.Container, "cnr1", []string{"ingress:myrule2", "ingress:myrule3"}) - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) - checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) - checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) - checkChain(t, c.policy, policy.Container, "cnr1", "ingress:myrule3", p33) - checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p33}) // Override chain. - checkChainsByPrefix(t, c.policy, policy.Container, "cnr1", "", [][]byte{p2, p33}) - checkChainsByPrefix(t, c.policy, policy.IAM, "", "", nil) - checkChainKeys(t, c.policy, policy.Container, "cnr1", []string{"ingress:myrule2", "ingress:myrule3"}) - - checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress:myrule3", [][]byte{p33}) - checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress", [][]byte{p2, p33}) + checkChainsIteratorByPrefix(t, e, policy.Container, "cnr1", "ingress:myrule3", [][]byte{p33}) + checkChainsIteratorByPrefix(t, e, policy.Container, "cnr1", "ingress", [][]byte{p2, p33}) t.Run("removal", func(t *testing.T) { t.Run("wrong name", func(t *testing.T) { - c.policy.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress") - checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1}) + e.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress") + checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) }) - c.policy.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress:123") - checkChains(t, c.policy, "mynamespace", "", "ingress", nil) - checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p2, p33}) // Container chains still exist. + e.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress:123") + checkChains(t, e, "mynamespace", "", "ingress", nil) + checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p2, p33}) // Container chains still exist. // Remove by prefix. - c.policy.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") - checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", nil) + e.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") + checkChains(t, e, "mynamespace", "cnr1", "ingress", nil) // Remove by prefix. - c.policy.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") - checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", nil) + e.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") + checkChains(t, e, "mynamespace", "cnr1", "ingress", nil) - checkTargets(t, c.policy, policy.Namespace, [][]byte{}) - checkTargets(t, c.policy, policy.Container, [][]byte{}) + checkTargets(t, e, policy.Namespace, [][]byte{}) + checkTargets(t, e, policy.Container, [][]byte{}) }) t.Run("add again after removal", func(t *testing.T) { - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) + e.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) + e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) - checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) - checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) - }) - - t.Run("add chain for non-active namespace", func(t *testing.T) { - c.frostfsid.Invoke(t, stackitem.Null{}, "createNamespace", "nsdisabled") - - c.frostfsid.Invoke(t, stackitem.Null{}, "updateNamespace", "nsdisabled", "frozen") - c.policy.InvokeFail(t, "namespace is non-active", "addChain", policy.Namespace, "nsdisabled", "ingress:3", p1) - - c.frostfsid.Invoke(t, stackitem.Null{}, "updateNamespace", "nsdisabled", "purge") - c.policy.InvokeFail(t, "namespace is non-active", "addChain", policy.Namespace, "nsdisabled", "ingress:3", p1) - - c.frostfsid.Invoke(t, stackitem.Null{}, "updateNamespace", "nsdisabled", "active") - c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "nsdisabled", "ingress:3", p1) + checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) + checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) }) } func TestAutorization(t *testing.T) { - c := newPolicyInvokers(t) + e := newPolicyInvoker(t) - c.policy.Invoke(t, stackitem.Null{}, "getAdmin") + e.Invoke(t, stackitem.Null{}, "getAdmin") - s := c.policy.NewAccount(t, 1_0000_0000) - cs := c.policy.WithSigners(s) + s := e.NewAccount(t, 1_0000_0000) + c := e.WithSigners(s) args := []any{policy.Container, "cnr1", "ingress:myrule3", []byte("opaque")} - cs.InvokeFail(t, policy.ErrNotAuthorized, "addChain", args...) + c.InvokeFail(t, policy.ErrNotAuthorized, "addChain", args...) - c.policy.Invoke(t, stackitem.Null{}, "setAdmin", s.ScriptHash()) - c.policy.Invoke(t, stackitem.NewBuffer(s.ScriptHash().BytesBE()), "getAdmin") + e.Invoke(t, stackitem.Null{}, "setAdmin", s.ScriptHash()) + e.Invoke(t, stackitem.NewBuffer(s.ScriptHash().BytesBE()), "getAdmin") - cs.Invoke(t, stackitem.Null{}, "addChain", args...) + c.Invoke(t, stackitem.Null{}, "addChain", args...) } func checkChains(t *testing.T, e *neotest.ContractInvoker, namespace, container, name string, expected [][]byte) {