package frostfsid import ( "git.frostfs.info/TrueCloudLab/frostfs-contract/common" "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/convert" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "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" ) type ( Subject struct { PrimaryKey interop.PublicKey AdditionalKeys []interop.PublicKey Namespace string Name string KV map[string]string } SubjectExtended struct { PrimaryKey interop.PublicKey AdditionalKeys []interop.PublicKey Namespace string Name string KV map[string]string Groups []Group } Namespace struct { Name string } NamespaceExtended struct { Name string GroupsCount int SubjectsCount int } Group struct { ID int Name string Namespace string KV map[string]string } GroupExtended struct { ID int Name string Namespace string KV map[string]string SubjectsCount int } ) const ( adminKey = 'o' subjectKeysPrefix = 's' additionalKeysPrefix = 'a' namespaceKeysPrefix = 'n' namespaceSubjectsKeysPrefix = 'N' namespaceSubjectsNamesPrefix = 'l' groupKeysPrefix = 'g' groupSubjectsKeysPrefix = 'G' groupCounterKey = 'c' namespaceGroupsNamesPrefix = 'm' ) func _deploy(data any, isUpdate bool) { ctx := storage.GetContext() args := data.(struct { admin interop.Hash160 }) if args.admin != nil { if len(args.admin) != interop.Hash160Len { panic("incorrect length of owner address") } storage.Put(ctx, adminKey, args.admin) } storage.Put(ctx, groupCounterKey, 0) storage.Put(ctx, namespaceKey(""), std.Serialize(Namespace{})) runtime.Log("frostfsid contract initialized") } func SetAdmin(addr interop.Hash160) { ctx := storage.GetContext() if !common.HasUpdateAccess() { panic("not witnessed") } storage.Put(ctx, adminKey, addr) } func ClearAdmin() { ctx := storage.GetContext() if !common.HasUpdateAccess() { panic("not witnessed") } storage.Delete(ctx, adminKey) } func GetAdmin() interop.Hash160 { ctx := storage.GetReadOnlyContext() return storage.Get(ctx, adminKey).(interop.Hash160) } // Update method updates contract source code and manifest. It can be invoked // only by committee. func Update(script []byte, manifest []byte, data any) { if !common.HasUpdateAccess() { panic("only committee can update contract") } management.UpdateWithData(script, manifest, common.AppendVersion(data)) runtime.Log("frostfsid contract updated") } // Version returns the version of the contract. func Version() int { return common.Version } func CreateSubject(ns string, key interop.PublicKey) { ctx := storage.GetContext() checkContractOwner(ctx) if len(key) != interop.PublicKeyCompressedLen { panic("incorrect public key length") } addr := contract.CreateStandardAccount(key) sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data != nil { panic("subject already exists") } saPrefix := subjectAdditionalPrefix(key) it := storage.Find(ctx, saPrefix, storage.KeysOnly) for iterator.Next(it) { panic("key is occupied") } nsKey := namespaceKey(ns) data = storage.Get(ctx, nsKey).([]byte) if data == nil { panic("namespace not found") } subj := Subject{ PrimaryKey: key, Namespace: ns, } storage.Put(ctx, sKey, std.Serialize(subj)) nsSubjKey := namespaceSubjectKey(ns, addr) storage.Put(ctx, nsSubjKey, []byte{1}) runtime.Notify("CreateSubject", interop.Hash160(addr)) } func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("incorrect address length") } if len(key) != interop.PublicKeyCompressedLen { panic("incorrect public key length") } saKey := subjectAdditionalKey(key, addr) data := storage.Get(ctx, saKey).([]byte) if data != nil { panic("key already added") } storage.Put(ctx, saKey, []byte{1}) sKey := subjectKeyFromAddr(addr) data = storage.Get(ctx, sKey).([]byte) if data == nil { panic("address not found") } subject := std.Deserialize(data).(Subject) subject.AdditionalKeys = append(subject.AdditionalKeys, key) storage.Put(ctx, sKey, std.Serialize(subject)) runtime.Notify("AddSubjectKey", addr, key) } func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("incorrect address length") } if len(key) != interop.PublicKeyCompressedLen { panic("incorrect public key length") } saKey := subjectAdditionalKey(key, addr) data := storage.Get(ctx, saKey).([]byte) if data == nil { panic("key already removed") } storage.Delete(ctx, saKey) sKey := subjectKeyFromAddr(addr) data = storage.Get(ctx, sKey).([]byte) if data == nil { panic("address not found") } subject := std.Deserialize(data).(Subject) 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]) } } subject.AdditionalKeys = additionalKeys storage.Put(ctx, sKey, std.Serialize(subject)) runtime.Notify("RemoveSubjectKey", addr, key) } func SetSubjectName(addr interop.Hash160, name string) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("incorrect address length") } sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } subject := std.Deserialize(data).(Subject) oldName := subject.Name subject.Name = name storage.Put(ctx, sKey, std.Serialize(subject)) updateNamespaceSubjectName(ctx, subject, oldName) runtime.Notify("SetSubjectName", addr, name) } func SetSubjectKV(addr interop.Hash160, key, val string) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("incorrect address length") } sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } subject := std.Deserialize(data).(Subject) if subject.KV == nil { subject.KV = map[string]string{} } subject.KV[key] = val storage.Put(ctx, sKey, std.Serialize(subject)) runtime.Notify("SetSubjectKV", addr, key, val) } func DeleteSubjectKV(addr interop.Hash160, key string) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("incorrect address length") } sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } subject := std.Deserialize(data).(Subject) delete(subject.KV, key) storage.Put(ctx, sKey, std.Serialize(subject)) runtime.Notify("DeleteSubjectKV", addr, key) } func DeleteSubject(addr interop.Hash160) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("incorrect address length") } sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { return } subj := std.Deserialize(data).(Subject) for i := 0; i < len(subj.AdditionalKeys); i++ { storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr)) } storage.Delete(ctx, sKey) removeSubjectFromNamespace(ctx, subj.Namespace, addr) deleteNamespaceSubjectName(ctx, subj.Namespace, subj.Name) runtime.Notify("DeleteSubject", addr) } func GetSubject(addr interop.Hash160) Subject { if len(addr) != interop.Hash160Len { panic("incorrect address length") } ctx := storage.GetReadOnlyContext() sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } return std.Deserialize(data).(Subject) } func GetSubjectExtended(addr interop.Hash160) SubjectExtended { subj := GetSubject(addr) ctx := storage.GetReadOnlyContext() var groups []Group nsHash := ripemd160Hash(subj.Namespace) it := storage.Find(ctx, groupPrefixFromHash(nsHash), storage.KeysOnly|storage.RemovePrefix) for iterator.Next(it) { groupHash := iterator.Value(it).([]byte) data := storage.Get(ctx, groupSubjectKeyFromHashes(nsHash, groupHash, addr)).([]byte) if data != nil { data = storage.Get(ctx, groupKeyFromHashes(nsHash, groupHash)).([]byte) if data == nil { panic("group not found") } groups = append(groups, std.Deserialize(data).(Group)) } } subjExt := SubjectExtended{ PrimaryKey: subj.PrimaryKey, AdditionalKeys: subj.AdditionalKeys, Namespace: subj.Namespace, Name: subj.Name, KV: subj.KV, Groups: groups, } return subjExt } func GetSubjectByKey(key interop.PublicKey) Subject { if len(key) != interop.PublicKeyCompressedLen { panic("incorrect key length") } ctx := storage.GetReadOnlyContext() sKey := subjectKey(key) data := storage.Get(ctx, sKey).([]byte) if data != nil { return std.Deserialize(data).(Subject) } saPrefix := subjectAdditionalPrefix(key) it := storage.Find(ctx, saPrefix, storage.KeysOnly|storage.RemovePrefix) for iterator.Next(it) { addr := iterator.Value(it).([]byte) sKey = subjectKeyFromAddr(addr) data = storage.Get(ctx, sKey).([]byte) if data != nil { return std.Deserialize(data).(Subject) } break } panic("subject not found") } func GetSubjectByName(ns, name string) Subject { key := GetSubjectKeyByName(ns, name) addr := contract.CreateStandardAccount(key) return GetSubject(addr) } func GetSubjectKeyByName(ns, name string) interop.PublicKey { if name == "" { panic("invalid or name") } ctx := storage.GetReadOnlyContext() nsSubjNameKey := namespaceSubjectNameKey(ns, name) subjKey := storage.Get(ctx, nsSubjNameKey).(interop.PublicKey) if subjKey == nil { panic("subject not found") } return subjKey } func ListSubjects() iterator.Iterator { ctx := storage.GetReadOnlyContext() return storage.Find(ctx, []byte{subjectKeysPrefix}, storage.KeysOnly|storage.RemovePrefix) } func CreateNamespace(ns string) { ctx := storage.GetContext() checkContractOwner(ctx) nsKey := namespaceKey(ns) data := storage.Get(ctx, nsKey).([]byte) if data != nil { panic("namespace already exists") } namespace := Namespace{ Name: ns, } storage.Put(ctx, nsKey, std.Serialize(namespace)) runtime.Notify("CreateNamespace", ns) } func GetNamespace(ns string) Namespace { ctx := storage.GetReadOnlyContext() nsKey := namespaceKey(ns) data := storage.Get(ctx, nsKey).([]byte) if data == nil { panic("namespace not found") } return std.Deserialize(data).(Namespace) } func GetNamespaceExtended(ns string) NamespaceExtended { ctx := storage.GetReadOnlyContext() nsKey := namespaceKey(ns) data := storage.Get(ctx, nsKey).([]byte) if data == nil { panic("namespace not found") } namespace := std.Deserialize(data).(Namespace) nsExtended := NamespaceExtended{ Name: namespace.Name, } it := storage.Find(ctx, groupPrefix(ns), storage.KeysOnly) for iterator.Next(it) { nsExtended.GroupsCount += 1 } it = storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly) for iterator.Next(it) { nsExtended.SubjectsCount += 1 } return nsExtended } func ListNamespaces() iterator.Iterator { ctx := storage.GetReadOnlyContext() return storage.Find(ctx, []byte{namespaceKeysPrefix}, storage.ValuesOnly|storage.DeserializeValues) } func ListNamespaceSubjects(ns string) iterator.Iterator { ctx := storage.GetReadOnlyContext() return storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly|storage.RemovePrefix) } func CreateGroup(ns, group string) int { ctx := storage.GetContext() checkContractOwner(ctx) if group == "" { panic("invalid group name") } nsKey := namespaceKey(ns) data := storage.Get(ctx, nsKey).([]byte) if data == nil { panic("namespace not found") } groupCountID := storage.Get(ctx, groupCounterKey).(int) groupCountID++ storage.Put(ctx, groupCounterKey, groupCountID) gr := Group{ ID: groupCountID, Name: group, Namespace: ns, } setNamespaceGroupName(ctx, gr) gKey := groupKey(ns, groupCountID) storage.Put(ctx, gKey, std.Serialize(gr)) runtime.Notify("CreateGroup", ns, group) return groupCountID } func GetGroup(ns string, groupID int) Group { ctx := storage.GetReadOnlyContext() gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } return std.Deserialize(data).(Group) } func GetGroupExtended(ns string, groupID int) GroupExtended { ctx := storage.GetReadOnlyContext() gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } gr := std.Deserialize(data).(Group) grExtended := GroupExtended{ ID: gr.ID, Name: gr.Name, Namespace: gr.Namespace, KV: gr.KV, } it := storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly) for iterator.Next(it) { grExtended.SubjectsCount += 1 } return grExtended } func GetGroupIDByName(ns, name string) int { if name == "" { panic("invalid name") } ctx := storage.GetReadOnlyContext() nsGroupNameKey := namespaceGroupNameKey(ns, name) groupIDRaw := storage.Get(ctx, nsGroupNameKey).([]byte) if groupIDRaw == nil { panic("group not found") } return std.Deserialize(groupIDRaw).(int) } func SetGroupName(ns string, groupID int, name string) { ctx := storage.GetContext() checkContractOwner(ctx) gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } gr := std.Deserialize(data).(Group) oldName := gr.Name gr.Name = name storage.Put(ctx, gKey, std.Serialize(gr)) updateNamespaceGroupName(ctx, gr, oldName) runtime.Notify("SetGroupName", ns, groupID, name) } func SetGroupKV(ns string, groupID int, key, val string) { ctx := storage.GetContext() checkContractOwner(ctx) gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } gr := std.Deserialize(data).(Group) if gr.KV == nil { gr.KV = map[string]string{} } gr.KV[key] = val storage.Put(ctx, gKey, std.Serialize(gr)) runtime.Notify("SetGroupKV", ns, groupID, key, val) } func DeleteGroupKV(ns string, groupID int, key string) { ctx := storage.GetContext() checkContractOwner(ctx) gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } gr := std.Deserialize(data).(Group) delete(gr.KV, key) storage.Put(ctx, gKey, std.Serialize(gr)) runtime.Notify("DeleteGroupKV", ns, groupID, key) } func ListGroups(ns string) iterator.Iterator { ctx := storage.GetReadOnlyContext() return storage.Find(ctx, groupPrefix(ns), storage.ValuesOnly|storage.DeserializeValues) } func AddSubjectToGroup(addr interop.Hash160, groupID int) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("invalid address length") } sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } subject := std.Deserialize(data).(Subject) gKey := groupKey(subject.Namespace, groupID) data = storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } group := std.Deserialize(data).(Group) if group.Namespace != subject.Namespace { panic("subject and group must be in the same namespace") } gsKey := groupSubjectKey(subject.Namespace, groupID, addr) storage.Put(ctx, gsKey, []byte{1}) runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, groupID) } func RemoveSubjectFromGroup(addr interop.Hash160, groupID int) { ctx := storage.GetContext() checkContractOwner(ctx) if len(addr) != interop.Hash160Len { panic("invalid address length") } sKey := subjectKeyFromAddr(addr) data := storage.Get(ctx, sKey).([]byte) if data == nil { panic("subject not found") } subject := std.Deserialize(data).(Subject) gKey := groupKey(subject.Namespace, groupID) data = storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } group := std.Deserialize(data).(Group) if group.Namespace != subject.Namespace { panic("subject and group must be in the same namespace") } gsKey := groupSubjectKey(subject.Namespace, groupID, addr) storage.Delete(ctx, gsKey) runtime.Notify("RemoveSubjectFromGroup", addr, subject.Namespace, groupID) } func ListGroupSubjects(ns string, groupID int) iterator.Iterator { ctx := storage.GetReadOnlyContext() return storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly|storage.RemovePrefix) } func DeleteGroup(ns string, groupID int) { ctx := storage.GetContext() checkContractOwner(ctx) gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } gr := std.Deserialize(data).(Group) storage.Delete(ctx, gKey) it := storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly) for iterator.Next(it) { gsKey := iterator.Value(it).([]byte) storage.Delete(ctx, gsKey) } deleteNamespaceGroupName(ctx, ns, gr.Name) runtime.Notify("DeleteGroup", ns, groupID) } func checkContractOwner(ctx storage.Context) { addr := storage.Get(ctx, adminKey) if addr != nil && runtime.CheckWitness(addr.(interop.Hash160)) { return } if common.HasUpdateAccess() { return } panic("not witnessed") } func removeSubjectFromNamespace(ctx storage.Context, ns string, addr interop.Hash160) { nsSubjKey := namespaceSubjectKey(ns, addr) storage.Delete(ctx, nsSubjKey) nsHash := ripemd160Hash(ns) it := storage.Find(ctx, groupPrefixFromHash(nsHash), storage.KeysOnly|storage.RemovePrefix) for iterator.Next(it) { groupHash := iterator.Value(it).([]byte) storage.Delete(ctx, groupSubjectKeyFromHashes(nsHash, groupHash, addr)) } } func setNamespaceSubjectName(ctx storage.Context, subj Subject) { if subj.Name == "" { return } nsSubjNameKey := namespaceSubjectNameKey(subj.Namespace, subj.Name) subjKey := storage.Get(ctx, nsSubjNameKey).(interop.PublicKey) if subjKey == nil { storage.Put(ctx, nsSubjNameKey, subj.PrimaryKey) } else if !common.BytesEqual(subjKey, subj.PrimaryKey) { panic("subject name is not available in the current namespace") } } func deleteNamespaceSubjectName(ctx storage.Context, ns, subjName string) { if subjName == "" { return } nsSubjNameKey := namespaceSubjectNameKey(ns, subjName) storage.Delete(ctx, nsSubjNameKey) } func updateNamespaceSubjectName(ctx storage.Context, subj Subject, oldName string) { if subj.Name == oldName { return } deleteNamespaceSubjectName(ctx, subj.Namespace, oldName) setNamespaceSubjectName(ctx, subj) } func updateNamespaceGroupName(ctx storage.Context, gr Group, oldName string) { if gr.Name == oldName { return } deleteNamespaceGroupName(ctx, gr.Namespace, oldName) setNamespaceGroupName(ctx, gr) } func deleteNamespaceGroupName(ctx storage.Context, ns, grName string) { if grName == "" { return } nsGroupNameKey := namespaceGroupNameKey(ns, grName) storage.Delete(ctx, nsGroupNameKey) } func setNamespaceGroupName(ctx storage.Context, gr Group) { if gr.Name == "" { return } nsGroupNameKey := namespaceGroupNameKey(gr.Namespace, gr.Name) groupIDRaw := storage.Get(ctx, nsGroupNameKey).([]byte) if groupIDRaw == nil { storage.Put(ctx, nsGroupNameKey, std.Serialize(gr.ID)) return } groupID := std.Deserialize(groupIDRaw).(int) if groupID != gr.ID { panic("group name is not available in the current namespace") } } func subjectKey(key interop.PublicKey) []byte { addr := contract.CreateStandardAccount(key) return subjectKeyFromAddr(addr) } func subjectKeyFromAddr(addr interop.Hash160) []byte { return append([]byte{subjectKeysPrefix}, addr...) } func subjectAdditionalKey(additionalKey interop.PublicKey, primeAddr interop.Hash160) []byte { return append(subjectAdditionalPrefix(additionalKey), primeAddr...) } func subjectAdditionalPrefix(additionalKey interop.PublicKey) []byte { addr := contract.CreateStandardAccount(additionalKey) return append([]byte{additionalKeysPrefix}, addr...) } func namespaceKey(ns string) []byte { return namespaceKeyFromHash(ripemd160Hash(ns)) } func namespaceKeyFromHash(ns []byte) []byte { return append([]byte{namespaceKeysPrefix}, ns...) } func groupKey(ns string, groupID int) []byte { prefix := groupPrefix(ns) return append(prefix, idToBytes(groupID)...) } func groupKeyFromHashes(nsHash []byte, groupIDBytes []byte) []byte { prefix := groupPrefixFromHash(nsHash) return append(prefix, groupIDBytes...) } func groupPrefix(ns string) []byte { return groupPrefixFromHash(ripemd160Hash(ns)) } func groupPrefixFromHash(nsHash []byte) []byte { return append([]byte{groupKeysPrefix}, nsHash...) } func ripemd160Hash(data string) []byte { return crypto.Ripemd160([]byte(data)) } func namespaceSubjectKey(ns string, addr interop.Hash160) []byte { prefix := namespaceSubjectPrefix(ns) return append(prefix, addr...) } func namespaceSubjectPrefix(ns string) []byte { nsHash := ripemd160Hash(ns) return append([]byte{namespaceSubjectsKeysPrefix}, nsHash...) } func namespaceSubjectNameKey(ns, subjName string) []byte { nsHash := ripemd160Hash(ns) nameHash := ripemd160Hash(subjName) return append([]byte{namespaceSubjectsNamesPrefix}, append(nsHash, nameHash...)...) } func namespaceGroupNameKey(ns, groupName string) []byte { nsHash := ripemd160Hash(ns) nameHash := ripemd160Hash(groupName) return append([]byte{namespaceGroupsNamesPrefix}, append(nsHash, nameHash...)...) } func groupSubjectKey(ns string, groupID int, addr interop.Hash160) []byte { prefix := groupSubjectPrefix(ns, groupID) return append(prefix, addr...) } func groupSubjectKeyFromHashes(nsHash, groupHash []byte, addr interop.Hash160) []byte { prefix := groupSubjectPrefixFromHashes(nsHash, groupHash) return append(prefix, addr...) } func groupSubjectPrefix(ns string, groupID int) []byte { nsHash := ripemd160Hash(ns) return groupSubjectPrefixFromHashes(nsHash, idToBytes(groupID)) } func groupSubjectPrefixFromHashes(nsHash, groupIDBytes []byte) []byte { return append([]byte{groupSubjectsKeysPrefix}, append(nsHash, groupIDBytes...)...) } // idToBytes converts i64 value to BE bytes. Panics if value is bigger than i64. func idToBytes(itemID int) []byte { b := convert.ToBytes(itemID) ln := len(b) if ln > 8 { panic("item ID exceeds 8 byte limit") } zeros := make([]byte, 8-ln) return append(b, zeros...) }